Why is the order of channels receiving causing/resolving a deadlock in Golang?

Issue

I’ve boiled my issue down to this simple example below. I am invoking a goroutine that takes two channels and sends one message to each. Then I am attempting to receive those messages further along. However, the order of channels receiving matters. If I use the same order I sent the messages, the program runs. If I switch, it does not.

I would have expected the goroutine to run independently from retrieving the messages, allowing me to receive from whichever channel I wanted to first.

I can solve this by sending messages to a single channel per goroutine (2 goroutines).

Could someone explain why there is an order dependence here and why 2 separate goroutines resolves that dependence?

package main

import "fmt"

func main() {
    chanA := make(chan string)
    chanB := make(chan string)

    go func() {
        chanA <- "el"
        chanB <- "el"
    }()

    // if B is received before A, fatal error
    // if A is received before B, completes
    <-chanB
    <-chanA

    fmt.Println("complete")
}

Solution

You will need to buffer your channels. A buffered channel can store so many elements before it will block.

chanA := make(chan string, 1)
chanA <- "el" // This will not block
fmt.Println("Hello World")

When you do chanA <- "el" on the buffered channel above, the element gets placed into the buffer and the thread does not block. If you add a second element, it will then block as there is no room in the buffer:

chanA := make(chan string, 1)
chanA <- "el"
chanA <- "el" // <- This will block, as the buffer is full

In your example, you have a buffer of 0. So the first write to the channel is blocked, and requires another thread to read the value to unblock.

https://go.dev/play/p/6GbsVW4d0Mg

    chanA := make(chan string)
    go func() {
        time.Sleep(time.Second)
        fmt.Println("Pop:", <-chanA) // Unblock the writer
    }()
    chanA <- "el"

Extra knowledge

If you do not want a thread to block, you can wrap a channel insert in a select. This will ensure if the channel is full, your application does not deadlock. One cheap way of fixing this is a larger buffer…

https://go.dev/play/p/kKR-lrCO4FX

    select {
    case chanA <- "el":
    default:
        return fmt.Errorf("value not written: %s", value)
    }

Answered By – Steven Masley

Answer Checked By – Mary Flores (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.