How do I avoid deadlocks and silent errors when using select?

Issue

I am learning Go by example. I’ve just implemented a select to await multiple channels as follows:

for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }

With a little experimentation I’ve found that I can naively introduce runtime errors as follows:

  • If I reduce i to 1, the first message is received, but the second is silently lost (there is no indication that I unwittingly ignored it).

  • If I increase i to 3, both messages are received but I get fatal error: all goroutines are asleep - deadlock!

Reading ahead and searching for that error message on StackOverflow I can see that WaitGroups account for these types of issues. But they don’t seem to apply to select, so I feel like I must be missing something.

Is there a language construct (like if/then/else) or software pattern that I can use to prevent or mitigate against these errors in real-world code?

Solution

Conceptually you mitigate against this by designing software correctly. If you have two channels, and each channel will receive at most one message, don’t try to read from them 3 times. This is no different than trying to put three items in a two element array, or trying to divide two numbers where the divisor is 0. In all these cases languages offer ways of discovering and recovering from the error, but if you’re actually producing these errors, it indicates a logic or design flaw.

You need to make sure that your channels have a balanced number of reads and writes, and that the sending end closes the channel when it has nothing else to send so receivers can stop waiting for messages that won’t come. Otherwise you’ll eventually have something stuck waiting, or messages in a buffer that are ignored.

In this very specific case, if you want to read from both channels but only if a message is ready, you can add a default case which will be invoked if no channel is ready for reading, but that’s for situations where your channels are not ready yet but will eventually become ready. Providing a default is not a good solution to cover over bugs where channels will never become ready yet you’re still trying to read from them; that indicates a logic-level flaw that needs to be fixed.

Answered By – meagar

Answer Checked By – Candace Johnson (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.