Search code examples
gogoroutine

Best approach to getting results out of goroutines


I have two functions that I cannot change (see first() and second() below). They are returning some data and errors (the output data is different, but in the examples below I use (string, error) for simplicity)

I would like to run them in separate goroutines - my approach:

package main

import (
    "fmt"
    "os"
)

func first(name string) (string, error) {
    if name == "" {
        return "", fmt.Errorf("empty name is not allowed")
    }
    fmt.Println("processing first")
    return fmt.Sprintf("First hello %s", name), nil
}

func second(name string) (string, error) {
    if name == "" {
        return "", fmt.Errorf("empty name is not allowed")
    }
    fmt.Println("processing second")
    return fmt.Sprintf("Second hello %s", name), nil
}

func main() {
    firstCh := make(chan string)
    secondCh := make(chan string)
    
    go func() {
        defer close(firstCh)
        res, err := first("one")
        if err != nil {
            fmt.Printf("Failed to run first: %v\n", err)
        }
        firstCh <- res
    }()

    go func() {
        defer close(secondCh)
        res, err := second("two")
        if err != nil {
            fmt.Printf("Failed to run second: %v\n", err)
        }
        secondCh <- res
    }()

    resultsOne := <-firstCh
    resultsTwo := <-secondCh

    // It's important for my app to do error checking and stop if errors exist.
    if resultsOne == "" || resultsTwo == "" {
        fmt.Println("There was an ERROR")
        os.Exit(1)
    }

    fmt.Println("ONE:", resultsOne)
    fmt.Println("TWO:", resultsTwo)
}

I believe one caveat is that resultsOne := <- firstCh blocks until first goroutine finishes, but I don't care too much about this.

Can you please confirm that my approach is good? What other approaches would be better in my situation?


Solution

  • The example looks mostly good. A couple improvements are:

    • declaring your channels as buffered
       firstCh := make(chan string, 1)
       secondCh := make(chan string, 1)
    

    With unbuffered channels, send operations block (until someone receives). If your goroutine #2 is much faster than the first, it will have to wait until the first finishes as well, since you receive in sequence:

        resultsOne := <-firstCh // waiting on this one first
        resultsTwo := <-secondCh // sender blocked because the main thread hasn't reached this point
    
    • use "golang.org/x/sync/errgroup".Group. The program will feel "less native" but it dispenses you from managing channels by hand — which trades, in a non-contrived setting, for sync'ing writes on the results:
    func main() {
        var (
            resultsOne string
            resultsTwo string
        )
    
        g := errgroup.Group{}
        
        g.Go(func() error {
            res, err := first("one")
            if err != nil {
                return err
            }
            resultsOne = res
            return nil
        })
    
        g.Go(func() error {
            res, err := second("two")
            if err != nil {
                return err
            }
            resultsTwo = res
            return nil
        })
    
        err := g.Wait()
        // ... handle err