Problem Statement
During a Google SRE interview, you're asked to explain the nuanced behavior of Go channels. A production outage was caused by a goroutine leak due to improper channel handling. Walk through all channel edge cases.
Channel Behavior Matrix
| Operation | Nil Channel | Closed Channel | Open Channel |
|---|---|---|---|
ch <- value | Blocks forever | PANIC | Sends or blocks |
<-ch | Blocks forever | Returns zero value + false | Receives or blocks |
close(ch) | PANIC | PANIC | Closes successfully |
The Goroutine Leak Scenario
package main
import (
"fmt"
"runtime"
"time"
)
func leakyFunction() {
ch := make(chan int)
go func() {
// This goroutine will block forever
// because no one ever sends to ch
val := <-ch
fmt.Println("Received:", val)
}()
// Function returns, but goroutine is stuck
}
func main() {
for i := 0; i < 1000; i++ {
leakyFunction()
}
time.Sleep(time.Second)
fmt.Printf("Goroutines: %d\n", runtime.NumGoroutine())
// Output: Goroutines: 1001 (leaked!)
}The Fix: Context-Based Cancellation
func properFunction(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case val := <-ch:
fmt.Println("Received:", val)
case <-ctx.Done():
fmt.Println("Cancelled, exiting goroutine")
return
}
}()
}Interview Questions
- Q: What happens if you read from a nil channel?
A: It blocks forever. This is actually useful in select statements to disable a case. - Q: Why would you intentionally use a nil channel?
A: To dynamically disable a select case without removing the code. - Q: How do you detect a closed channel?
A: Use the two-value receive:val, ok := <-ch. Ifokis false, channel is closed.
Real-World Production Issue
At Twitch, a service leaked 50,000 goroutines because websocket handlers were waiting on a channel that was never closed when clients disconnected. The fix: Always use context cancellation for cleanup.