Reading errors from multiple channels

Issue

Like many go programmers, have so far avoided doing anything significant with channels so this apparently simple scenario has got me stumped!

I want multiple goroutines to send results checked by a single parent. If any send an error, the parent should signal for them all to stop. The parent should read the results non-sequentially.

This code works provided one of the goroutines does send an error i.e. if you comment in the 11 inside nums otherwise we just get stuck in the for loop forever.

func main() {
    type Result struct {
        Error    error
        Response int
    }
    checkStatus := func(done <-chan interface{}, ns []int) <-chan Result {
        results := make(chan Result)
        go func() {
            defer close(results)
            for _, n := range ns {
                result := Result{Response: n}
                if n == 11 {
                    result.Error = fmt.Errorf("problem...\n")
                }
                select {
                case <-done:
                    return
                case results <- result:
                }
            }
        }()
        return results
    }

    done := make(chan interface{})
    defer close(done)

    nums := []int{1, 2, 3, 4, 5 /*11,*/, 6, 7, 8, 9, 10}
    c1 := checkStatus(done, nums[:5])
    c2 := checkStatus(done, nums[5:])
    for {
        var r Result
        select {
        case r = <-c1:
            if r.Error != nil {
                fmt.Printf("error1: %v", r.Error)
                return
            }
            fmt.Printf("Response1: %v\n", r.Response)
        case r = <-c2:
            if r.Error != nil {
                fmt.Printf("error2: %v", r.Error)
                return
            }
            fmt.Printf("Response2: %v\n", r.Response)
        }
    }
}

The only way I can see to fix it is the change the for loop so it reads from c1 and c2 but I can’t see a way to do this non-sequentially?

https://go.dev/play/p/7dRPMDn1Za2

Solution

You’re reading from closed channels and they always return a zero value. What you can do is to turn off a select case by using a comma ok idiom while reading from a channel and then assign the channel to nil. If the other one is nil as well then return.

case with a nil channel never runs.

Just by extending your code (similarly for the case with c2):

case r, ok := <-c1:
    if !ok {
        c1 = nil
        if c2 == nil {
            return
        }
        continue
    }
    if r.Error != nil {
        fmt.Printf("error1: %v", r.Error)
        return
    }
    fmt.Printf("Response1: %v\n", r.Response)

But I’d rather try to refactor the whole implementation.

What you can consider is using sync.WaitGroup from the sync pkg or errgroup.Group from the errgroup pkg.

Answered By – Erni27

Answer Checked By – Cary Denson (GoLangFix Admin)

Leave a Reply

Your email address will not be published.