Search code examples
gocoroutinegoroutine

Pattern for fetching multiple fields in parallel


I need to fetch multiple fields in parallel for my system from external services (in this example, simulated by Name(), Age() and CanDrive() methods).

The fetchUser() method does what I want, but it seems too verbose specially if you consider I could have 10+ fields. Are there better ways I can implement this?

playground: https://play.golang.org/p/90sNq1GmrD8

Code (same as in playground):

package main

import (
    "fmt"
    "sync"
)

type User struct {
    Name string
    Age int
    CanDrive *bool
}

func Name() (string, error) {
    return "foobar", nil
}

func Age() (int, error) {
    return 25, nil
}

func CanDrive() (bool, error) {
    return true, nil
}

func fetchUser() (*User, error) {
    var wg sync.WaitGroup
    errs := make(chan error)

    user := &User{}

    wg.Add(1)
    go func() {
        var err error
        defer wg.Done()
        user.Name, err = Name()
        errs <- err
    }()

    wg.Add(1)

    go func() {
        var err error
        defer wg.Done()
        user.Age, err = Age()
        errs <- err
    }()

    wg.Add(1)
    go func() {
        defer wg.Done()
        canDrive, err := CanDrive()
        if err == nil {
            user.CanDrive = &canDrive
        }
        errs <- err
    }()

    // wait until all go-routines are completed successfully
    // if that's the case, close the errs channel
    go func() {
        wg.Wait()
        close(errs)
    }()

    // keep waiting for errors (or for the error channel to be closed
    // if all calls succeed)
    for err := range errs {
        if err != nil {
            return nil, err
        }
    }

    return user, nil
}

func main() {
    user, _ := fetchUser()
    fmt.Println(user)
}

Solution

  • Without knowing more of the specifics of your scenario, my only suggestion would be to separate out the Go routine error handling into another package.

    Fortunately, a package already exists that does the same thing, named errgroup. Below is an implementation of your original code using the errgroup package:

    package main
    
    import (
        "context"
        "fmt"
    
        "golang.org/x/sync/errgroup"
    )
    
    type User struct {
        Name     string
        Age      int
        CanDrive *bool
    }
    
    func Name() (string, error) {
        return "foobar", nil
    }
    
    func Age() (int, error) {
        return 25, nil
    }
    
    func CanDrive() (bool, error) {
        return true, nil
    }
    
    func fetchUser(ctx context.Context) (*User, error) {
        group, ctx := errgroup.WithContext(ctx)
    
        user := &User{}
        group.Go(func() (err error) {
            user.Name, err = Name()
            return
        })
        group.Go(func() (err error) {
            user.Age, err = Age()
            return
        })
        group.Go(func() error {
            canDrive, err := CanDrive()
            if err == nil {
                user.CanDrive = &canDrive
            }
            return err
        })
    
        if err := group.Wait(); err != nil {
            return nil, err
        }
    
        return user, nil
    }
    
    func main() {
        user, err := fetchUser(context.Background())
        fmt.Println(user, err)
    }