Best approach to getting results out of goroutines

Issue

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

Answered By – blackgreen

Answer Checked By – Gilberto Lyons (GoLangFix Admin)

Leave a Reply

Your email address will not be published.