Go cheatsheet covering the go toolchain, syntax essentials, goroutines and channels, table-driven …
Go Concurrency Cheatsheet Go Concurrency Cheatsheet

Summary
Go makes starting a goroutine so cheap that the hard part moves somewhere else: knowing when they stop, and what they are allowed to touch on the way. This sheet is the small set of shapes I reuse — worker pool, select with context, WaitGroup — and the three failures that cost me the most time: the race that prints the right answer, the deadlock, and the leak. The language basics are on the Go Cheatsheet.
Goroutines
Docs: Effective Go: concurrency
Expand your knowledge with Go (Golang) Cheatsheet
go doWork() // fire and forget — usually a bug
go func() { doWork() }() // same, with a closure
var wg sync.WaitGroup
for _, host := range hosts {
wg.Add(1) // before the go statement, never inside it
go func() {
defer wg.Done() // defer, so a panic still decrements
check(host) // Go 1.22+: loop var is per-iteration
}()
}
wg.Wait()
go func(h string){...}(host). From 1.22 each iteration
gets its own copy and the plain closure is correct. Check the go directive in
your go.mod — it is the language version that decides, not the toolchain.The Race Detector
Docs: Data Race Detector
This is the one to internalise. The program below is wrong, and it prints the right answer anyway:
restarts := 0
var wg sync.WaitGroup
for range 100 {
wg.Add(1)
go func() { defer wg.Done(); restarts++ }() // unsynchronised write
}
wg.Wait()
fmt.Println("restarts:", restarts)
$ go run race.gorestarts: 100restarts: 100# Two runs, both correct. Nothing here will ever make you suspicious.$ echo $? → 0$ go run -race race.go==================WARNING: DATA RACERead at 0x00c000012168 by goroutine 14: main.main.func1() /home/user/fleet/race.go:13 +0x84 Previous write at 0x00c000012168 by goroutine 12: main.main.func1() /home/user/fleet/race.go:13 +0x96Found 2 data race(s)exit status 66-race in CI — go test -race ./... — because that is
the only place it will reliably be found.The fix is a mutex or an atomic, not a bigger channel:
Deepen your understanding in Go Security Cheatsheet
var mu sync.Mutex
mu.Lock(); restarts++; mu.Unlock()
var restarts atomic.Int64 // simpler for a plain counter
restarts.Add(1)
restarts.Load()
Channels
Docs: Go tour: channels
Explore this further in Go (Golang) Cheatsheet
ch := make(chan string) // unbuffered: send blocks until a receive
ch := make(chan string, 10) // buffered: blocks only when full
ch <- "deploy" // send
v := <-ch // receive
v, ok := <-ch // ok is false once closed and drained
close(ch) // sender closes. Never the receiver.
for v := range ch { ... } // ends when the channel is closed
| Operation | On a nil channel | On a closed channel |
|---|---|---|
| send | blocks forever | panics |
| receive | blocks forever | returns the zero value immediately |
| close | panics | panics |
$ go run dead.gofatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]:main.main() /home/user/fleet/dead.go:5 +0x28exit status 2# An unbuffered send with nobody receiving. The runtime can only detect this# when EVERY goroutine is stuck — one leaked goroutine among many is silent.$ echo $? → 2select
Docs: Go tour: select
select {
case v := <-ch1: // whichever is ready first
use(v)
case ch2 <- x:
case <-time.After(5 * time.Second): // timeout
return errTimeout
case <-ctx.Done(): // cancellation
return ctx.Err()
default: // non-blocking: runs if nothing is ready
}
Several ready at once means one is chosen at random, not in source order.
An empty select {} blocks forever.
Discover related concepts in Bash CLI One-Liners Cheatsheet
context
Docs: context package
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // always, even on the success path
ctx, cancel := context.WithCancel(parent)
ctx, cancel := context.WithDeadline(parent, t)
ctx, cancel := context.WithTimeoutCause(parent, d, errSlowRegion) // 1.21+
context.Context is the first parameter, named ctx, and never stored in a
struct. Cancelling a parent cancels every child.
Uncover more details in Go Logging Cheatsheet (log/slog)
$ go run ctx.goreturned after 100ms: context deadline exceededis DeadlineExceeded: truefunc slowCall(ctx context.Context) error {
select {
case <-time.After(300 * time.Millisecond): // the real work
return nil
case <-ctx.Done():
return ctx.Err() // DeadlineExceeded, or Canceled
}
}
select is the only thing that makes a goroutine cancellable. A goroutine
in the middle of a blocking call that does not take a context — a bare
time.Sleep, a read on a socket with no deadline — will not stop when the
context is cancelled. It keeps running and the caller just stops waiting.Worker Pool
The shape to reach for when there are more jobs than you want goroutines.
Journey deeper into this topic with Python Threading & Concurrency Cheatsheet
jobs := make(chan string)
results := make(chan string)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ { // three workers, not one per host
wg.Add(1)
go func(id int) {
defer wg.Done()
for h := range jobs { // ends when jobs is closed
results <- check(id, h)
}
}(w)
}
go func() { wg.Wait(); close(results) }() // close results once workers end
go func() {
for _, h := range hosts { jobs <- h }
close(jobs) // the signal to stop
}()
for r := range results { collect(r) }
$ go run pool.gocompleted: 5close(jobs) and the workers block in range forever, wg.Wait()
never returns, and the program hangs with no error. Miss the
go func(){ wg.Wait(); close(results) }() and the final range results
hangs instead. Both are silent.sync
Docs: sync package
Enrich your learning with Bash CLI One-Liners Cheatsheet
var mu sync.Mutex
mu.Lock(); defer mu.Unlock()
var rw sync.RWMutex
rw.RLock() // many readers, or one writer
rw.Lock()
var once sync.Once
once.Do(func() { initClient() }) // exactly once, even across goroutines
initClient := sync.OnceFunc(setup) // 1.21+, same idea as a value
var m sync.Map // only for high-contention disjoint keys
The slogan is “share memory by communicating”, which is good advice and not a rule. In practice:
| Situation | Reach for |
|---|---|
| Protecting a struct field or a counter | sync.Mutex, or atomic |
| Handing work between stages | a channel |
| One value computed once, read by many | sync.Once |
| Waiting for N things to finish | sync.WaitGroup |
| Limiting concurrency to N | a buffered channel as a semaphore |
sem := make(chan struct{}, 10) // at most 10 in flight
for _, h := range hosts {
sem <- struct{}{} // acquire; blocks at 10
go func() {
defer func() { <-sem }() // release
check(h)
}()
}
A mutex guarding a map is simpler, faster and easier to read than a goroutine owning the map behind a channel. Use the channel when the handoff is the point, not when the shared state is.
Goroutine Leaks
A goroutine blocked forever is never collected, and it holds everything it references. The usual cause is a send on a channel nobody is reading any more:
@@ -1,9 +1,11 @@ func first(urls []string) string {- ch := make(chan string) // unbuffered+ ch := make(chan string, len(urls)) // room for every sender for _, u := range urls { go func() { ch <- get(u) }() } return <-ch // takes one, returns, abandons the rest }With an unbuffered channel the other goroutines block on the send forever.
Buffering to len(urls) lets them all finish; a context the caller cancels is
the other answer.
curl localhost:6060/debug/pprof/goroutine?debug=1 # counts, with stacks
go test -race ./... # in CI, always
Pick a service you run. If its goroutine count climbed slowly all week, would anything tell you — or would you find out when it was OOM-killed?
Gain comprehensive insights from Go (Golang) Cheatsheet
Related Cheatsheets
- Go Cheatsheet — syntax, toolchain and stdlib
- Go Logging Cheatsheet — logging from many goroutines without interleaving
- Python Threading & Concurrency Cheatsheet — the same problems with a GIL in the way
Similar Articles
Related Content
More from development
Python basics and CLI cheatsheet: core syntax, f-strings, comprehensions, pathlib, venv, pip, …
Quick-reference Bash basics cheatsheet: variables, parameter expansion, conditionals, loops, arrays, …
You Might Also Like
Go log/slog cheatsheet: structured logging, text and JSON handlers, levels, With and Group, context …
Go security cheatsheet: exec.Command without a shell, crypto/rand vs math/rand, SQL parameters, path …
Knowledge Quiz
Test your general knowledge with this quick quiz!
A set of multiple-choice questions to test your knowledge.
Take as much time as you need.
Your score will be shown at the end.
Question 1 of 5
Quiz Complete!
Your score: 0 out of 5
Loading next question...

