← Back to docs
Recipe

Goroutines Primer

Goroutines are Go's lightweight concurrency primitive. Where threads cost megabytes and milliseconds to spawn, goroutines cost kilobytes and microseconds. This recipe walks you through the three patterns you will reach for daily: fire-and-forget, bounded worker pools, and graceful shutdown with context.Context.

1.Fire and forget

Prefix any function call with the go keyword and it runs on a fresh goroutine. The caller does not wait. Use this for telemetry, logging, or any side effect whose result you do not need to observe.

func main() {
    go logEvent("user.signin")
    go warmCache(userID)
    handleRequest(w, r) // continues immediately
}

2.Bounded worker pools

Unbounded go calls are how production outages start. Cap concurrency with a buffered channel acting as a semaphore, or spawn a fixed pool that drains a job channel. The pool pattern keeps memory flat under load spikes.

jobs := make(chan Job, 100)
for i := 0; i < 8; i++ {
    go func() {
        for job := range jobs {
            process(job)
        }
    }()
}

3.Graceful shutdown

Every long-lived goroutine should accept a context.Contextand exit when it is cancelled. Pair this with sync.WaitGroupso the main routine can block until every worker has flushed its state. This is the difference between a clean rolling deploy and dropped in-flight requests.

func worker(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            return
        case job := <-jobs:
            process(job)
        }
    }
}