Goroutines
A goroutine is a lightweight thread managed by the Go runtime — start one with the go keyword.
Introduction
A goroutine is a lightweight thread managed by the Go runtime — start one with the go keyword. Goroutines multiplex onto OS threads (M:N scheduling), enabling millions of concurrent tasks with kilobytes of stack each.
Goroutines power HTTP servers (one per request), worker pools, background jobs, and streaming pipelines. Without synchronization (channels, WaitGroup, mutex), shared state races corrupt data — interviewers always probe this gap.
This lesson covers spawning goroutines, the GOMAXPROCS setting, and common patterns for graceful shutdown with context cancellation.
The story
Google's gRPC servers handle each incoming RPC in its own goroutine — lightweight green threads scheduled by the Go runtime across OS threads. A Docker registry mirror spawns goroutines per layer download, saturating network bandwidth without the memory overhead of one OS thread per connection.
Goroutines communicate via channels (next lesson), but launching them is trivial: go processRequest(req) — the concurrency model that makes Go the default language for cloud-native infrastructure.
Understanding the topic
Key concepts
- go func() starts goroutine — non-blocking, runs concurrently.
- Main goroutine exiting kills all goroutines — wait before exit.
- runtime.GOMAXPROCS sets OS thread count; defaults to CPU cores.
- Goroutine stack starts ~2KB, grows dynamically.
- Closure captures variables by reference — mind loop vars (fixed Go 1.22).
- Leak: goroutine blocked forever on channel — always plan exit path.
flowchart TBMain[main goroutine] --> G1[go worker 1]Main --> G2[go worker 2]G1 --> Done[sync.WaitGroup]G2 --> Done
Step-by-step explanation
- go statement schedules function on runtime scheduler.
- Scheduler assigns goroutines to GOMAXPROCS logical processors.
- Blocking syscall may spawn additional OS thread.
- Main returns → program exits regardless of background goroutines.
- Use sync.WaitGroup or channel to synchronize completion.
- context.Context signals cancellation across goroutine tree.
Practical code example
Worker pool spawning goroutines with WaitGroup and context:
package mainimport ("context""fmt""sync""time")func worker(ctx context.Context, id int, jobs <-chan int, wg *sync.WaitGroup) {defer wg.Done()for {select {case <-ctx.Done():returncase job, ok := <-jobs:if !ok {return}fmt.Printf("worker %d processed job %d\n", id, job)time.Sleep(50 * time.Millisecond)}}}func main() {ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)defer cancel()jobs := make(chan int, 10)var wg sync.WaitGroupfor i := 1; i <= 3; i++ {wg.Add(1)go worker(ctx, i, jobs, &wg)}for j := 1; j <= 5; j++ {jobs <- j}close(jobs)wg.Wait()}
Line-by-line code explanation
go handleRequest(conn)starts a new goroutine — non-blocking, returns immediately.runtime.NumGoroutine()reports active goroutines — useful in leak diagnostics.sync.WaitGroup(covered later) waits for a batch of goroutines to finish.goroutine stacks start small (~2 KB)and grow dynamically — cheaper than OS threads.closure capture— loop variables in goroutines need careful copying (Go 1.22+ fixes loop var semantics).panic in goroutinecrashes the whole program unless recovered — always handle errors.GOMAXPROCScontrols parallelism across CPU cores — defaults toruntime.NumCPU().main exits when main goroutine finishes— background goroutines are killed unless waited on.
Key takeaway: Always provide cancellation path. close(jobs) signals no more work. WaitGroup ensures main waits for workers.
Real-world use
Where you'll use this in production
- net/http handles each request in a goroutine automatically.
- Background email/notification senders decoupled from API response.
- Parallel health checks aggregating microservice status.
- Fan-out/fan-in data processing pipelines.
Best practices
- Every goroutine must have a planned exit (context, close, timeout).
- Use WaitGroup or errgroup for coordinated shutdown.
- Don't spawn unbounded goroutines — use worker pools with limits.
- Pass dependencies as params, not global vars.
- Monitor goroutine count in production via runtime/metrics or pprof.
Common mistakes
- Main exits before goroutines finish — incomplete work.
- Goroutine leak on blocked channel send/receive.
- Unbounded go per incoming request without backpressure.
- Race on shared variables without sync or channels.
- Capturing loop variable in closure pre-1.22.
Advanced interview questions
Q1BeginnerGoroutine vs OS thread?
Q2BeginnerWhat happens if main returns while goroutines run?
Q3IntermediateGOMAXPROCS effect?
Q4IntermediateDetect goroutine leak?
Q5AdvancedDesign worker pool processing 10K jobs/sec.
Summary
go keyword launches lightweight concurrent goroutines. Main must wait for workers — WaitGroup, channels, or errgroup. Always plan goroutine exit to prevent leaks. Use worker pools instead of unbounded goroutine spawning. Next lesson: channels — communicating between goroutines.