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.
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
<-ch1is 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
gostatement 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:
<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
5without deadlocking.
The key lesson is to avoid placing channel operations that can block in the argument list of a
gostatement; such operations are evaluated in the launching goroutine, not the new one.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.