Go Cheatsheet
Channels
Use this Go reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
What Is a Channel
A channel is a typed conduit for communication between goroutines. Sending and receiving block until the other side is ready (for unbuffered channels).
ch := make(chan int) // unbuffered ch := make(chan string, 5) // buffered, capacity 5 var ch chan int // nil channel (send/receive block forever)
Send, Receive, Close
ch := make(chan int, 2) ch <- 42 // send (blocks if full for buffered, or until receiver for unbuffered) v := <-ch // receive (blocks if empty) v, ok := <-ch // ok is false when channel is closed and drained close(ch) // signal no more values will be sent; only the sender should close
Sending on a closed channel panics. Receiving from a closed, drained channel returns the zero value and
ok=false.
Unbuffered vs Buffered
// Unbuffered: send blocks until a receiver is ready (synchronous handoff) ch := make(chan int) go func() { ch <- 1 }() fmt.Println(<-ch) // 1 // Buffered: send blocks only when buffer is full ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 // ch <- 4 // would block fmt.Println(len(ch)) // 3 (current items in buffer) fmt.Println(cap(ch)) // 3 (buffer capacity)
Ranging Over a Channel
for range reads until the channel is closed.
ch := make(chan int, 5) for i := 0; i < 5; i++ { ch <- i } close(ch) for v := range ch { fmt.Println(v) // 0, 1, 2, 3, 4 } // Loop exits when ch is closed and drained
Directional Channels
Restrict a channel to send-only or receive-only in function signatures.
func producer(out chan<- int) { // send-only for i := 0; i < 5; i++ { out <- i } close(out) } func consumer(in <-chan int) { // receive-only for v := range in { fmt.Println(v) } } func main() { ch := make(chan int, 5) go producer(ch) // bidirectional chan implicitly converts consumer(ch) }
select Statement
Waits on multiple channel operations; picks one at random if multiple are ready.
select { case v := <-ch1: fmt.Println("from ch1:", v) case v := <-ch2: fmt.Println("from ch2:", v) case ch3 <- "hello": fmt.Println("sent to ch3") default: fmt.Println("no channel ready (non-blocking)") }
Timeout
select { case result := <-work: fmt.Println(result) case <-time.After(3 * time.Second): fmt.Println("timed out") }
Tick / ticker
ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for { select { case t := <-ticker.C: fmt.Println("tick at", t) case <-done: return } }
Done Channel Pattern (Cancellation)
func worker(done <-chan struct{}) { for { select { case <-done: fmt.Println("stopping") return default: // do work } } } done := make(chan struct{}) go worker(done) time.Sleep(1 * time.Second) close(done) // broadcast stop to all readers
Pipeline Pattern
Each stage is a goroutine connected by channels.
func gen(nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- n } }() return out } func sq(in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range in { out <- n * n } }() return out } for v := range sq(sq(gen(2, 3))) { fmt.Println(v) // 16, 81 }
Fan-out / Fan-in
// Fan-out: distribute one channel to multiple goroutines func fanOut(in <-chan int, n int) []<-chan int { outs := make([]<-chan int, n) for i := range outs { out := make(chan int) outs[i] = out go func(o chan<- int) { defer close(o) for v := range in { o <- v } }(out) } return outs } // Fan-in: merge multiple channels into one func merge(cs ...<-chan int) <-chan int { var wg sync.WaitGroup out := make(chan int) forward := func(c <-chan int) { defer wg.Done() for v := range c { out <- v } } wg.Add(len(cs)) for _, c := range cs { go forward(c) } go func() { wg.Wait() close(out) }() return out }
Channel as Semaphore
Limit concurrent access with a buffered channel.
const maxConcurrent = 5 sem := make(chan struct{}, maxConcurrent) for _, task := range tasks { sem <- struct{}{} // acquire go func(t Task) { defer func() { <-sem }() // release process(t) }(task) } // Wait for all to finish for i := 0; i < cap(sem); i++ { sem <- struct{}{} }
Channel as Signal / Event
// Ready signal ready := make(chan struct{}) go func() { setup() close(ready) // broadcast: anyone blocking on <-ready is unblocked }() <-ready // wait // Single-value notification notify := make(chan struct{}, 1) notify <- struct{}{} // non-blocking send (buffered)
Nil Channel Tricks
A nil channel blocks forever on both send and receive. Use in select to disable a case.
var ch1, ch2 <-chan int = make(chan int), nil // ch2 case is permanently disabled select { case v := <-ch1: fmt.Println("ch1:", v) case v := <-ch2: // never selected fmt.Println("ch2:", v) }
// Drain a channel and then disable it func merge(a, b <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for a != nil || b != nil { select { case v, ok := <-a: if !ok { a = nil; continue } out <- v case v, ok := <-b: if !ok { b = nil; continue } out <- v } } }() return out }
Gotchas
// 1. Deadlock: all goroutines blocked ch := make(chan int) ch <- 1 // main goroutine blocks; no receiver → DEADLOCK // fatal error: all goroutines are asleep - deadlock! // 2. Panic: send on closed channel close(ch) ch <- 1 // PANIC // 3. Panic: close of nil channel var ch chan int close(ch) // PANIC // 4. Panic: close closed channel ch := make(chan int) close(ch) close(ch) // PANIC // 5. Goroutine leak: reader goroutine blocks forever if channel never closed // Always ensure a goroutine has a way to exit (context, done channel, close) // 6. Buffered channel not a queue for long-term storage // it is still in-memory and blocks when full
Channel vs Mutex — Quick Guide
| Use | Approach |
|---|---|
| Passing ownership of data | Channel |
| Coordinating multiple goroutines | Channel |
| Simple shared state (counter, flag, cache) | Mutex |
| Semaphore / rate limiting | Buffered channel |
| One-time event / signal | close(ch) |
| Recurring events | time.Ticker |
| Concurrent map access | sync.Map or sync.RWMutex |