Go Goroutines & Channels Cheat Sheet
Covers launching goroutines with the go keyword, sending and receiving on channels, buffered versus unbuffered channels, and select statements.
Basic Goroutines
Starting concurrent work with the go keyword.
func sayHello() { fmt.Println("Hello from goroutine")}func main() { go sayHello() // Starts a new goroutine (non-blocking) time.Sleep(100 * time.Millisecond) // Naive wait; use sync primitives in real code}
Channels
Typed pipes for communicating between goroutines.
ch := make(chan int) // Unbuffered channelgo func() { ch <- 42 // Send value (blocks until received)}()value := <-ch // Receive value (blocks until sent)buffered := make(chan string, 3) // Buffered channel, capacity 3buffered <- "a"buffered <- "b"close(buffered) // Close when no more values will be sentfor v := range buffered { // Range reads until the channel is closed fmt.Println(v)}
sync.WaitGroup & select
Waiting for goroutines and multiplexing channel operations.
var wg sync.WaitGroupfor i := 0; i < 3; i++ { wg.Add(1) go func(id int) { defer wg.Done() fmt.Println("worker", id) }(i)}wg.Wait() // Block until all goroutines call Done()select {case msg := <-ch1: fmt.Println("from ch1:", msg)case msg := <-ch2: fmt.Println("from ch2:", msg)case <-time.After(1 * time.Second): fmt.Println("timeout")default: fmt.Println("no message ready")}
Concepts
Vocabulary for Go's concurrency model.
- go keyword- Launches a function call as a new lightweight goroutine, managed by the Go runtime
- Unbuffered channel- Send blocks until a receiver is ready (a synchronous handoff)
- Buffered channel- Send blocks only once the buffer is full
- close(ch)- Signals no more values will be sent; receiving from a closed channel returns the zero value and false
- sync.WaitGroup- Waits for a collection of goroutines to finish (Add/Done/Wait)
- sync.Mutex- Protects shared state from concurrent access (Lock/Unlock)
- select- Waits on multiple channel operations, choosing pseudo-randomly if more than one is ready
context.Context for Cancellation & Timeouts
The idiomatic way to propagate cancellation and deadlines across goroutines.
func fetch(ctx context.Context, url string) error { ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err // ctx.Err() would be context.DeadlineExceeded on timeout } defer resp.Body.Close() return nil}func worker(ctx context.Context, jobs <-chan int) { for { select { case <-ctx.Done(): fmt.Println("cancelled:", ctx.Err()) return case j, ok := <-jobs: if !ok { return } fmt.Println("processing", j) } }}
Worker Pool Pattern
Bounding concurrency while fanning work out across a fixed number of goroutines.
func workerPool(jobs <-chan int, results chan<- int, numWorkers int) { var wg sync.WaitGroup for w := 0; w < numWorkers; w++ { wg.Add(1) go func(id int) { defer wg.Done() for j := range jobs { // Exits when jobs channel is closed and drained results <- j * j } }(w) } go func() { wg.Wait() close(results) // Safe to close: only after all workers finish }()}// Usagejobs := make(chan int, 100)results := make(chan int, 100)workerPool(jobs, results, 4)for i := 1; i <= 10; i++ { jobs <- i}close(jobs)for r := range results { fmt.Println(r)}
errgroup for Concurrent Error Handling
Running goroutines that can fail and propagating the first error with cancellation.
import "golang.org/x/sync/errgroup"func fetchAll(ctx context.Context, urls []string) ([]string, error) { g, ctx := errgroup.WithContext(ctx) results := make([]string, len(urls)) for i, url := range urls { i, url := i, url // Capture loop variables (pre-Go 1.22) g.Go(func() error { body, err := fetchOne(ctx, url) if err != nil { return err // First non-nil error cancels the group's ctx } results[i] = body return nil }) } if err := g.Wait(); err != nil { return nil, err } return results, nil}
sync.Once, sync.RWMutex & atomic
Lower-level synchronization primitives for hot paths where a channel is overkill.
var ( once sync.Once instance *Config)func GetConfig() *Config { once.Do(func() { // Guaranteed to run exactly once, even under concurrent calls instance = loadConfig() }) return instance}type Counter struct { mu sync.RWMutex value int}func (c *Counter) Read() int { c.mu.RLock() // Multiple readers can hold the lock simultaneously defer c.mu.RUnlock() return c.value}var hits int64func RecordHit() { atomic.AddInt64(&hits, 1) // Lock-free increment, cheaper than a mutex for simple counters}
Pipeline Pattern with Directional Channels
Composing stages that each read from and write to typed, directional channels.
func generate(nums ...int) <-chan int { // Returns a receive-only channel out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- n } }() return out}func square(in <-chan int) <-chan int { // Receive-only in, receive-only out out := make(chan int) go func() { defer close(out) for n := range in { out <- n * n } }() return out}// Chain stages: generate -> square -> squarefor v := range square(square(generate(2, 3, 4))) { fmt.Println(v) // 16, 81, 256}
Concurrency Gotchas & Diagnostics
Mistakes that compile fine but fail (or deadlock) at runtime.
- Loop variable capture- Pre-Go 1.22, goroutines closing over a loop variable share one address; shadow it with i := i inside the loop
- -race flag- go run -race / go test -race instruments the binary to detect data races at runtime; use it in CI
- Sending on a closed channel- Panics immediately; only the sender should ever close a channel, never the receiver
- Double close- Closing an already-closed channel panics; guard with sync.Once if multiple goroutines might close it
- Nil channel- Send/receive on a nil channel blocks forever; useful in select to disable a case dynamically
- Goroutine leak- A goroutine blocked forever on a channel nobody writes to (or reads from) never gets garbage collected
- GOMAXPROCS- Controls how many OS threads can execute Go code simultaneously; defaults to the number of CPU cores
"Don't communicate by sharing memory; share memory by communicating" — prefer channels to pass ownership of data between goroutines instead of protecting shared variables with mutexes wherever practical.