1
\$\begingroup\$

I'm trying to understand an SO answer for this question Golang select statement with channels and waitgroup.

The author of the now accepted answer goes on to say that OP's code is almost similar to this, when the function is replaced with the actual code

package main

import (
    "errors"
    "fmt"
)

func main() {
    errs := make(chan error, 1)
    success := make(chan bool, 1)
    errs <- errors.New("Some error")
    success <- true

    select {
    case err := <-errs:
        fmt.Println("error", err)
    case <-success:
        fmt.Println("success")
    }

    select {
    case err := <-errs:
        fmt.Println("error", err)
        // panic(err)
    case <-success:
        fmt.Println("success")
    }
}

What I don't understand is why are there two select wait operations? I understand that both the error and bool channels are written at the same time and there could be one handled by the select at any time, but why does OP have two instances select? Would like to understand the reason behind possibly a better way to achieve the same functionality.

\$\endgroup\$
3
\$\begingroup\$

Not sure whether this site is the best place to ask this question, but the answer is quite simple. I'll answer the question, give a few alternatives, pro's and con's and some actual code-review on the little snippet you provided.

Do not communicate by sharing memory; instead, share memory by communicating

This is the main idea behind channels. When talking about communicating and sharing memory, it's obvious we're talking about concurrency. The example you have isn't really dealing with concurrency all that much. Such an artificial example will almost always be fairly limit in explaining the mechanisms/concepts behind something that is, effectively, a weapon in the armoury of concurrent programming. Either way, worth checking out the effective go document for more on this.


To answer your question:

Looking at the select spec:

A "select" statement chooses which of a set of possible send or receive operations will proceed.

Then, from the steps that a select statement executes, there's 2 steps to pay attention to:

  1. one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the "select" statement blocks until at least one of the communications can proceed.

And:

  1. The statement list of the selected case is executed.

This means that, even if both channels are ready to be read/received from, the select statement will only ever read from a single channel. Which channel that is, is not specified (other than the channel is chosen via a uniform pseudo-random selection).

If you'd have:

errs := make(chan error, 1)
success := make(chan struct{}, 1)
defer func() {
    close(errs)
    close(success)
}()
errs <- errors.New("some error")
success <- struct{}{}

select {
case err := <-errs:
    fmt.Printf("Receiver error: %+v\n", err)
case <-success:
    fmt.Println("Success received")
}

You would have no way of knowing which channel you'd read from.


Some code review:

You may have noticed, too, that I've changed a couple of things. This being a code-review site and all, here's why I made the changes I did:

  1. Success or done channels are either unbuffered (indicating success/done on close), or are best defined as chan struct{}. A boolean can be false or true, indicating that the value does have a meaning to its receiver. Again as per spec, a type struct{} is defined as 0 bytes in size (slightly more optimised than bool), and clearly indicates the channel serves to signal something, rather than communicate something.
  2. errors.New("Some error") should be "some error". Error values should start with a lower-case letter. Check golang CodeReviewComments for details. There's a lot of conventions there, and overall the community seems to have adopted them.
  3. I added a defer func(){}() to close the channels. It's bad form to not close channels...

Either way, the double select statements are there to ensure both channels are read.


More details

Be that as it may, having a quick look at the linked post, seeing as a waitgroup is used in the accepted answer, I'd probably prefer something more like this:

errs := make(chan error) // not buffered
success := make(chan struct{}) // not buffered
go func() {
    errs <- errors.New("some error")
}()
go func() {
    success <- struct{}{}
}()
err := <-errs
fmt.Printf("Received error: %+v\n", err)
close(errs) // we're done with this channel
<-success
close(success) // all done
fmt.Println("Success")

The trade-off being: You will always wait for the error channel before reading the success channel. The advantage: no waitgroup required, no verbose select statement...

Should you want to maintain the random-like output (having success and error printed out in a random order), you can easily replace the ordered reads from the channels 2 select statements, or use a waitgroup and read from the channels in routines:

func main() {
    errs := make(chan error) // not buffered
    success := make(chan struct{}) // not buffered
    wg := sync.WaitGroup{}
    // start reading from channels in routines
    wg.Add(2)
    go readRoutine(&wg, errs)
    go readRoutine(&wg, success)
    // routines are needed because the channels aren't buffered, writes are blocking...
    go func() {
        errs <- errors.New("some error")
    }()
    go func() {
        success <- struct{}{}
    }()
    wg.Wait() // wait for reads to have happened
    close(errs) // close channels
    close(success)
}

func readRoutine(wg *sync.WaitGroup, ch <-chan interface{}) {
    defer wg.Done()
    v := <-ch
    if err, ok := v.(error); ok {
        fmt.Printf("Received error: %+v\n", err)
        return
    }
    fmt.Println("Received success")
}

Now we're almost there... The rule of thumb is that the routine creating the channels should also be responsible for closing the channels. However, we know that the anonymous routines are the only 2 that are writing to the channels, and they are created and spawned by main, and they even access its scope. There's 2 changes we can make, therefore:

go func() {
    errs <- errors.New("some error")
    close(errs) // we can close this channel
}()
go func() {
    close(success) // we don't even have to write to this channel
}()
wg.Wait()
// channels are already closed

Hope this answered your question, provided some useful links to resources that can help you answer future questions, and maybe gave you a few ideas of how to use channels.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.