2

Within a golang package I am writing, I often have to make 2 HTTP requests to get the data I require.

My package contains client functions, which usually have 0 arguments and return a struct and an error.

func (c Client) GetProduct() (*Product, error)

However, I would like to be able to write a generic function, which could accept two of these client functions as arguments, run them concurrently and simply return the structs filled with data from the API I am hitting.

Ideally, I would like the function call to look like this:

struct1, struct2, err := rrunFunctions(client.GetProduct, client.GetSizes)

So far I have written the following generic function: https://go.dev/play/p/IA8LJqY0FPe

The problem is that because GetProduct and GetSizes both return a struct and error rather than an interface{} and error I get the following error at compile time:

./prog.go:54:28: cannot use client.GetProduct (value of type func() (*Product, error)) as type func() (interface{}, error) in argument to runFunctions
./prog.go:54:47: cannot use client.GetSizes (value of type func() (*Sizes, error)) as type func() (interface{}, error) in argument to runFunctions

My question is how do I get past this? Is writing a function generic as this possible in go?

Any general tips on using concurrency in this manner would also be appreciated.

slatermorgan
  • 145
  • 6

1 Answers1

3

Yes, this is possible using generics. Use 2 type parameters for the 2 return types of the functions:

func runFunctions[T1, T2 any](
    f1 func() (T1, error),
    f2 func() (T2, error),
) (res1 T1, res2 T2, err error) {
    var wg sync.WaitGroup
    wg.Add(2)

    var err1, err2 error

    go func() {
        defer wg.Done()
        res1, err1 = f1()
    }()

    go func() {
        defer wg.Done()
        res2, err2 = f2()
    }()

    wg.Wait()

    // check errors
    if err1 != nil {
        err = err1
        return
    }
    if err2 != nil {
        err = err2
        return
    }

    return
}

Notes:

  • Use defer to call wg.Done()
  • You must check errors after wg.Wait()!

Using it also becomes simpler as you don't have to deal with type assertions:

func main() {
    defer timeTrack(time.Now(), "Fetching products with sizes")
    fmt.Println("Starting synchronous calls...")

    client := &Client{}

    prodModel, sizesModel, err := runFunctions(client.GetProduct, client.GetSizes)
    if err != nil {
        // return error
    }

    build(prodModel, sizesModel)
}

This will output (try it on the Go Playground):

Starting synchronous calls...
product 123 has size S
product 123 has size M
product 123 has size L
2009/11/10 23:00:05 Fetching products with sizes took 5s
icza
  • 389,944
  • 63
  • 907
  • 827