Why This Simple Go Channel Code Always Deadlocks and How to Fix It
The article explains why a minimal 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 the go statement, and shows how restructuring the code eliminates the deadlock.
We examine a Go program that appears straightforward but always 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 go run a.go produces the runtime error:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/a.go:10 +0x65
exit status 2Repeated execution (even with a bash loop running the program thousands of times) never succeeds, confirming that the deadlock is deterministic and not caused by execution order.
Changing the channel to a buffered one (capacity 100) does not alter the outcome:
func main() {
ch1 := make(chan int, 100)
go fmt.Println(<-ch1)
ch1 <- 5
time.Sleep(1 * time.Second)
}The root cause is that the receive operation <-ch1 inside the go fmt.Println(<-ch1) call is evaluated in the main goroutine before the new goroutine starts. Consequently, the main goroutine blocks waiting for a value that cannot be sent because the goroutine that would send it has not yet begun.
The function value and parameters are evaluated as usual in the calling goroutine, but unlike a regular call, program execution does not wait for the invoked function to complete.
The Go language specification also states that function arguments are evaluated before the function call, except for the special case of method receivers.
Thus the sequence of events is:
Main goroutine reaches the go statement.
The argument <-ch1 is evaluated in the main goroutine.
The new goroutine is created, but the receive has already blocked the main goroutine.
The program deadlocks because the send ch1 <- 5 cannot execute.
A generic deadlock pattern can be abstracted as:
func main() {
ch1 := make(chan int) // buffer size irrelevant
_ = <-chan // receive from an empty channel
ch1 <- 5
}Since the receive occurs before any send, the program inevitably deadlocks.
To avoid the deadlock, the receive must happen in a different goroutine from the one that performs the go statement. One fix is to wrap the call in an anonymous function so that the argument evaluation happens inside the new goroutine:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
go func() {
fmt.Println(<-ch1)
}()
ch1 <- 5
time.Sleep(1 * time.Second)
}In this version, the receive <-ch1 is executed inside the newly created goroutine, allowing the main goroutine to send the value and the program to terminate without deadlocking.
The lesson is to ensure that channel operations are performed in separate goroutines when using the go statement, especially when the operation involves receiving from a channel.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
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.
