What Does This Go Channel Code Actually Print? A Step‑by‑Step Interview Guide
The article explains how to reliably predict the output of Go channel interview questions by drawing execution timelines, distinguishing synchronization points from print order, handling buffered vs unbuffered channels, and spotting deadlocks, providing four concrete heuristics backed by code examples and runtime observations.
Predicting Output of a Simple Unbuffered Channel
Consider the following program:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int) // unbuffered
go func() {
ch <- 1
fmt.Println("goroutine: sent")
}()
val := <-ch
fmt.Println("main: received", val)
time.Sleep(time.Second)
}The four answer choices are:
goroutine: sent before main: received 1
main: received 1 before goroutine: sent
Order is nondeterministic
Deadlock
Many choose option 1, assuming the goroutine runs first. The correct analysis uses a timeline:
t0: main registers goroutine (go func(){...})
t1: main reaches "val := <-ch" and blocks waiting to receive
t2: goroutine runs, executes "ch <- 1" – handshake occurs, both sides unblock
t3: both sides resume; main receives 1, goroutine proceeds to print
t4: main prints "main: received 1"; goroutine prints "goroutine: sent"Key point: unbuffered channel send is synchronous – the send completes only when a receiver is ready. After the handshake, the two fmt.Println calls may run in either order because the scheduler can interleave them.
Running the program repeatedly shows the order goroutine: sent then main: received 1 in practice, but the Go specification does not guarantee this ordering; it is an implementation detail of the current scheduler.
Buffered Channel Example
Changing the channel to a buffer of size 2 alters the behavior:
func main() {
ch := make(chan int, 2) // buffered with capacity 2
ch <- 1 // non‑blocking, buffer now [1]
ch <- 2 // non‑blocking, buffer now [1,2]
go func() {
fmt.Println("goroutine: read", <-ch)
fmt.Println("goroutine: read", <-ch)
}()
ch <- 3 // blocks until a value is received
fmt.Println("main: sent 3")
time.Sleep(time.Second)
}Timeline (shown as plain text):
t0: main sends 1 → buffer [1]
t1: main sends 2 → buffer [1,2]
t2: goroutine is registered
t3: main attempts "ch <- 3" → blocks (buffer full)
t4: goroutine receives 1, buffer becomes [2]
t5: main unblocked, sends 3 → buffer [2,3]
t6: goroutine receives 2, prints
t7: main prints "main: sent 3"In ~99.5% of runs the output is:
goroutine: read 1
goroutine: read 2
main: sent 3Rarely the print order can differ because the print statements are not atomic with the channel operations.
Scheduling Order Is Not Guaranteed
Another interview question forces a single‑CPU scheduler with runtime.GOMAXPROCS(1) but still shows nondeterministic start order:
func main() {
runtime.GOMAXPROCS(1)
ch := make(chan int, 1)
done := make(chan bool)
go func() {
fmt.Println("A: start")
ch <- 1
fmt.Println("A: after send")
<-done
fmt.Println("A: done")
}()
go func() {
fmt.Println("B: start")
v := <-ch
fmt.Println("B: got", v)
done <- true
}()
time.Sleep(time.Second)
}Observed output (consistent across runs):
B: start
A: start
A: after send
B: got 1
A: doneThe runtime decides which goroutine runs first; the specification does not guarantee any start order. However, the synchronization points ( ch <- 1 and <-ch) create a deterministic handshake, while the surrounding fmt.Println calls can be reordered.
Deadlock Pitfall
A classic deadlock example:
func main() {
ch := make(chan int) // unbuffered
ch <- 1 // main sends with no receiver → deadlock
fmt.Println(<-ch)
}Running this program yields the runtime panic:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:The fix is to ensure a receiving goroutine exists, e.g., wrapping the receive in a go func(){ fmt.Println(<-ch) }() so the send can complete.
Four Heuristics for Channel Output Questions
Heuristic 1: Never infer execution order from the order of go statements; instead, draw a timeline marking who blocks and who wakes.
Heuristic 2: For buffered channels, always check the current buffer occupancy; a send blocks only when the buffer is full, not when it is empty.
Heuristic 3: Channel synchronization points are deterministic, but the prints that follow are not because of scheduler gaps.
Heuristic 4: A direct send on an unbuffered channel in the main goroutine without a matching receiver leads to deadlock.
Immediate Actions
Run the third example (the scheduling‑order question) ten times on your machine to see the start order vary.
Search any Go project for make(chan and examine unbuffered channels that are sent from the main goroutine; verify a corresponding receiver exists.
Re‑apply the "draw a timeline" method to a previously missed interview question, marking each send/recv as blocked or awakened.
These steps reinforce the mental model and dramatically improve success on Go concurrency interview problems.
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.
Tinker Programmer
Solving problems with code, sharing practical tech insights, and leveling up together!
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.
