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.

asked Apr 19, 2020 at 15:10
\$\endgroup\$

1 Answer 1

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.

answered Apr 20, 2020 at 10:12
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.