Backend Development 7 min read

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:

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

Running the program produces the runtime error

fatal error: all goroutines are asleep - deadlock!

. The cause is that the channel receive expression

&lt;-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:

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

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(&lt;-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:

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

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.

ConcurrencydeadlockGogoroutinechannel
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

login 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.