Go Cheatsheet
Goroutines
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 Goroutine
A goroutine is a lightweight thread managed by the Go runtime. Goroutines are multiplexed onto OS threads; creating thousands is normal.
func work(id int) { fmt.Printf("worker %d done\n", id) } func main() { go work(1) // launch goroutine go work(2) go func() { // anonymous goroutine fmt.Println("anonymous") }() time.Sleep(100 * time.Millisecond) // crude wait (use WaitGroup in real code) }
goprecedes any function call. The call returns immediately; the goroutine runs concurrently.
sync.WaitGroup
The standard way to wait for a collection of goroutines to finish.
import "sync" var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) // increment before launching go func(id int) { defer wg.Done() // decrement when goroutine exits fmt.Println("worker", id) }(i) } wg.Wait() // block until counter reaches 0
Common mistake: calling wg.Add(1) inside the goroutine — a race between the launch and the wait.
sync.Mutex
Protects shared data from concurrent access. Zero value is an unlocked mutex.
import "sync" type SafeCounter struct { mu sync.Mutex n int } func (c *SafeCounter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.n++ } func (c *SafeCounter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.n } c := &SafeCounter{} var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done(); c.Inc() }() } wg.Wait() fmt.Println(c.Value()) // 1000
sync.RWMutex
Multiple concurrent readers OR one exclusive writer.
type Cache struct { mu sync.RWMutex store map[string]string } func (c *Cache) Get(key string) (string, bool) { c.mu.RLock() defer c.mu.RUnlock() v, ok := c.store[key] return v, ok } func (c *Cache) Set(key, val string) { c.mu.Lock() defer c.mu.Unlock() c.store[key] = val }
sync.Once
Runs a function exactly once, regardless of how many goroutines call it. Used for lazy initialization.
var ( instance *DB once sync.Once ) func GetDB() *DB { once.Do(func() { instance = openDB() }) return instance }
sync.Map
Concurrent map with no external locking needed. Preferred for read-heavy or key-stable workloads.
var sm sync.Map sm.Store("key", 42) v, ok := sm.Load("key") // (42, true) sm.LoadOrStore("key", 99) // returns existing (42, true) sm.Delete("key") sm.Range(func(k, v any) bool { fmt.Println(k, v) return true // return false to stop })
sync.Cond
Condition variable for signaling between goroutines.
var mu sync.Mutex cond := sync.NewCond(&mu) ready := false // Consumer go func() { mu.Lock() for !ready { cond.Wait() // atomically releases mu and suspends } mu.Unlock() fmt.Println("ready!") }() // Producer mu.Lock() ready = true cond.Signal() // wake one waiter (or cond.Broadcast() for all) mu.Unlock()
atomic Package
Lock-free operations on primitive values. Faster than a mutex for simple counters/flags.
import "sync/atomic" var counter int64 // Increment atomic.AddInt64(&counter, 1) // Load / Store v := atomic.LoadInt64(&counter) atomic.StoreInt64(&counter, 0) // Compare-and-swap swapped := atomic.CompareAndSwapInt64(&counter, 0, 1) // Atomic value (any type) var val atomic.Value val.Store(myStruct{}) v2 := val.Load().(myStruct) // Go 1.19+ typed atomics var cnt atomic.Int64 cnt.Add(1) cnt.Load() cnt.Store(0) cnt.Swap(5) cnt.CompareAndSwap(5, 10) var flag atomic.Bool flag.Store(true) flag.Load()
Goroutine Patterns
Fan-out (scatter work)
results := make([]chan int, 10) for i := range results { ch := make(chan int, 1) results[i] = ch go func(id int, out chan<- int) { out <- heavyWork(id) }(i, ch) } for _, ch := range results { fmt.Println(<-ch) }
Worker pool
jobs := make(chan int, 100) var wg sync.WaitGroup // Start N workers for w := 0; w < 5; w++ { wg.Add(1) go func() { defer wg.Done() for j := range jobs { process(j) } }() } // Send jobs for j := 0; j < 100; j++ { jobs <- j } close(jobs) // signal workers to stop wg.Wait()
Pipeline
func generate(nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- n } }() return out } func square(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 square(generate(2, 3, 4)) { fmt.Println(v) // 4, 9, 16 }
Context-based cancellation
import "context" func longOp(ctx context.Context) error { for { select { case <-ctx.Done(): return ctx.Err() // context.Canceled or DeadlineExceeded default: // do work } } } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() err := longOp(ctx)
GOMAXPROCS
Controls the number of OS threads executing goroutines in parallel. Defaults to the number of CPU cores.
import "runtime" runtime.GOMAXPROCS(runtime.NumCPU()) // already the default prev := runtime.GOMAXPROCS(1) // single-threaded fmt.Println("CPUs:", runtime.NumCPU()) fmt.Println("goroutines:", runtime.NumGoroutine())
Race Detector
Run with -race to detect data races at runtime.
go run -race main.go go test -race ./... go build -race -o myapp
Common Gotchas
// 1. Loop variable capture (Go < 1.22) for i := 0; i < 5; i++ { go func() { fmt.Println(i) // RACE: all goroutines may see i=5 }() } // Fix: shadow or pass as argument for i := 0; i < 5; i++ { i := i // shadow go func() { fmt.Println(i) }() } // In Go 1.22+ loop variables are per-iteration — no fix needed // 2. Goroutine leak: always ensure goroutines can exit // Use context cancellation or a done channel // 3. Goroutines are not threads — you can have millions // but blocking syscalls tie up an OS thread // 4. defer wg.Done() in goroutines prevents missed Done() on panic go func() { defer wg.Done() // ... }()