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

UseApproach
Passing ownership of dataChannel
Coordinating multiple goroutinesChannel
Simple shared state (counter, flag, cache)Mutex
Semaphore / rate limitingBuffered channel
One-time event / signalclose(ch)
Recurring eventstime.Ticker
Concurrent map accesssync.Map or sync.RWMutex