Why This Simple Go Program Deadlocks: The Hidden Goroutine Trap

The article explains why a seemingly straightforward Go program that creates an unbuffered channel, launches a goroutine to print a received value, and then sends a value deadlocks, analyzes the evaluation order of go statements, and shows how to fix the issue by moving the receive into a separate goroutine.

Raymond Ops
Raymond Ops
Raymond Ops
Why This Simple Go Program Deadlocks: The Hidden Goroutine Trap

We examine a Go snippet that appears simple but results in a deadlock:

package main
import (
    "fmt"
    "time"
)
func main() {
    ch1 := make(chan int)
    go fmt.Println(<-ch1)
    ch1 <- 5
    time.Sleep(1 * time.Second)
}

Running the program produces the runtime error fatal error: all goroutines are asleep - deadlock!. The cause is that the channel receive expression <-ch1 is evaluated in the main goroutine before the new goroutine starts, so the main goroutine blocks waiting for a value that can never be sent.

Experiments with a buffered channel (capacity 100) and repeated runs confirm that the deadlock persists, proving that execution order is not the issue.

The pattern can be abstracted as:

func main() {
    ch1 := make(chan int) // buffer size irrelevant
    _ = <-chan // receive from empty channel
    ch1 <- 5
}

This inevitably deadlocks because the receive blocks the only goroutine, preventing the subsequent send.

According to the Go language spec, the arguments of a go statement are evaluated in the calling goroutine before the new goroutine begins execution:

The function value and parameters are evaluated as usual in the calling goroutine, but unlike with a regular call, program execution does not wait for the invoked function to complete.
Calls f with arguments a1, a2, … an. Except for one special case, arguments must be single‑valued expressions assignable to the parameter types of f and are evaluated before the function is called.

Thus, the receive operation in go fmt.Println(<-ch1) happens in the main goroutine, causing the deadlock.

Fixing the code by moving the receive into an explicit anonymous goroutine ensures the receive runs in a different goroutine:

package main
import (
    "fmt"
    "time"
)
func main() {
    ch1 := make(chan int)
    go func() {
        fmt.Println(<-ch1)
    }()
    ch1 <- 5
    time.Sleep(1 * time.Second)
}

Now the program prints 5 without deadlocking.

The key lesson is to avoid placing channel operations that can block in the argument list of a go statement; such operations are evaluated in the launching goroutine, not the new one.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

deadlockChannel
Raymond Ops
Written by

Raymond Ops

Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.