Starting a goroutine
Put go in front of a function call and that call runs concurrently. The statement returns immediately; the caller does not wait.
Four goroutines run shout at the same time. The order they finish in is not defined, but the output is always in the original order, because each goroutine writes to its own index and main only reads the slice after wg.Wait() returns.
That example already contains the three things almost every goroutine program needs: a way to start work (go), a way to wait for it (sync.WaitGroup), and a way to get results back without two goroutines touching the same memory (one slice element each).
Main does not wait
When main returns, the program exits. Goroutines that are still running are stopped wherever they are. Nothing waits for them.
This usually prints only from main. Sometimes the goroutine gets scheduled in time and you see both lines. That "usually" is the problem: code that works on your machine and fails on a loaded server.
A time.Sleep at the end of main makes the demo print both lines, and it is the wrong fix. It guesses how long the work takes. Wait for the work itself, with a WaitGroup (the WaitGroup page covers it in detail) or with a channel.
Getting results back
A go statement throws away the function's return values. x := go f() does not compile. There are two standard ways to return data.
One slot per goroutine, as in the first example. Pre-size a slice, give each goroutine its index, read after Wait. The order is preserved and no locking is needed, because no two goroutines write the same element.
A channel. Each goroutine sends its result; the receiver collects them. Results arrive in completion order, not start order.
Receiving exactly len(nums) values doubles as the wait: main cannot get past the loop until every goroutine has sent. The arrival order changes between runs, so the program sorts before printing anything order-dependent. The channels page covers buffered channels, closing, and range over a channel.
Loop variables and closures (Go 1.22 change)
Since Go 1.22, each iteration of a for loop gets a fresh copy of the loop variables. A closure started in a goroutine captures that iteration's value, so this is correct:
for i, w := range words {
go func() {
results[i] = shout(w) // Go 1.22+: i and w belong to this iteration
}()
}
Before Go 1.22, all iterations shared one i and one w, and every goroutine tended to see the last value. Old code works around it by passing the values as arguments, go func(i int, w string) { ... }(i, w), or by shadowing, i := i. Both are harmless on Go 1.22 and later, and you will still see them in existing code. The new behavior applies when the module's go.mod says go 1.22 or higher.
Goroutines are cheap
A goroutine starts with a small stack (a few kilobytes) that the runtime grows and shrinks as needed. The Go scheduler runs goroutines on a pool of OS threads, at most GOMAXPROCS of them executing Go code at once, and by default GOMAXPROCS equals the number of CPUs. Blocking on a channel, a mutex, a sleep or network I/O parks the goroutine and frees the thread for another one.
So starting a goroutine per task is fine even at large numbers:
A hundred thousand goroutines finish in a fraction of a second. The sum is always 4999950000, because atomic.Int64 makes each addition indivisible. Cheap does not mean free, though: each goroutine that is still blocked keeps its stack and everything it references alive.
| OS thread | Goroutine | |
|---|---|---|
| Created by | the kernel | the Go runtime |
| Initial stack | fixed, often 1 MB or more | a few KB, grows on demand |
| Switching | kernel context switch | Go scheduler, in user space |
| Identity | has a thread ID | no ID you can read, by design |
| Typical count | hundreds | thousands to millions |
Data races
Two goroutines that access the same variable at the same time, with at least one of them writing, is a data race. The result is unpredictable, not just "a bit off": updates get lost, and a race on a string, slice, map or interface value can crash the program or corrupt memory.
On a multi-core machine this prints a different number below 10000 on most runs, because two goroutines read the same old value and both write back that value plus one. On a single core it can print 10000, which is worse: the bug passes your test and shows up in production.
Go ships a race detector. Run your program or tests with -race:
go run -race main.go
go test -race ./...
==================
WARNING: DATA RACE
Read at 0x00c000090038 by goroutine 8:
main.main.func1()
/tmp/race/main.go:16 +0x94
Previous write at 0x00c000090038 by goroutine 6:
main.main.func1()
/tmp/race/main.go:16 +0xa4
...
Found 2 data race(s)
exit status 66
It points at the exact line (counter++) and both goroutines. It only reports races that actually happen during the run, so run it on tests that exercise the concurrent paths. It slows the program down several times, so it is for tests and staging, not production.
The fixes, from simplest to most general:
- Do not share. Give each goroutine its own data and combine at the end (the slot-per-goroutine pattern).
- Use
sync/atomicfor a single counter or flag:var n atomic.Int64; n.Add(1). - Use a
sync.Mutexaround anything bigger, like a map or a struct with several fields. The mutex page coversRWMutexandsync.Oncetoo. - Send the data over a channel so only one goroutine owns it at a time.
A panic in a goroutine kills the program
If a goroutine panics and nothing recovers inside that same goroutine, the whole program crashes, including main and every other goroutine. A recover in main does not help, because recover only catches panics in its own goroutine.
Recovering like this makes sense at the edge of a long-running server, where one bad request must not take down the rest. Inside ordinary code, a panic usually means a bug, and crashing loudly is the right outcome.
Goroutine leaks
A goroutine that blocks forever never exits and never frees its memory. The classic cause is a send that nobody will ever receive:
func firstResult(urls []string) string {
ch := make(chan string) // unbuffered
for _, u := range urls {
go func() { ch <- fetch(u) }()
}
return <-ch // takes the first result; the other senders block forever
}
Every call leaks len(urls) - 1 goroutines. In a server that handles this request thousands of times, memory climbs until the process dies. Two fixes: make the channel big enough that every sender can finish (make(chan string, len(urls))), or give the goroutines a way to give up, usually a context.Context plus a select on ctx.Done(). You can watch for leaks with runtime.NumGoroutine() in tests.
Limiting how many run at once
"One goroutine per item" is fine for 10,000 cheap computations. It is not fine for 10,000 HTTP requests to the same server or 10,000 open files. Cap the concurrency with a buffered channel used as a semaphore:
The buffered channel holds at most 3 tokens, so at most 3 goroutines are past the sem <- line at any moment. The peak can never exceed 3, and with twelve tasks that each sleep it reaches 3 in practice. A fixed pool of worker goroutines reading from a jobs channel is the other common shape; the WaitGroup page builds one.
Outside the standard library, golang.org/x/sync/errgroup combines a WaitGroup, the first error, context cancellation and a concurrency limit (g.SetLimit(n)) in one type. It is the usual choice in production code that needs all four.
Common mistakes
- Forgetting to wait.
mainreturns and the work silently never happens. Everygostatement needs a matching way to know it finished. - Calling
wg.Addinside the goroutine.Waitmay run beforeAdd, see a counter of zero and return early. CallAddbefore thegostatement. - Sharing a variable without synchronization. Maps are the common case: concurrent writes to a map are usually detected by the runtime and crash the program with
fatal error: concurrent map writes, whichrecovercannot catch. - Assuming an order. Goroutines run in whatever order the scheduler picks. If output must be ordered, collect and sort, or write to indexed slots.
- Using
time.Sleepto synchronize. It makes tests slow and still flaky. Wait on the event, not on a guess. - Starting a goroutine without a way to stop it. Anything that loops or waits on I/O should take a
context.Contextso the caller can cancel it.
Frequently Asked Questions
What is a goroutine in Go?
A goroutine is a function call that runs concurrently with the rest of the program. You start one by putting go before a call: go work(). Goroutines are managed by the Go runtime, not the operating system, and the runtime multiplexes many of them onto a small number of OS threads, so starting thousands of them is normal.
How do I wait for goroutines to finish in Go?
Use a sync.WaitGroup: call wg.Add(1) before each go statement, defer wg.Done() at the top of the goroutine, and wg.Wait() where you need all of them finished. If the goroutines produce values, receiving one value per goroutine from a channel also works as a wait.
What is the difference between a goroutine and a thread?
An OS thread has a fixed stack (often 1 MB or more) and is scheduled by the kernel. A goroutine starts with a stack of a few kilobytes that grows as needed, and the Go scheduler switches between goroutines in user space. The runtime runs goroutines on up to GOMAXPROCS threads at once (by default, the number of CPUs).
How do I get a return value from a goroutine?
A go statement discards the function's return values. Send the result on a channel (results <- compute(x)) or write it into your own slot of a pre-sized slice (out[i] = compute(x)) and read it after wg.Wait().
Why does my Go program exit before the goroutine prints anything?
When main returns, the program ends and every other goroutine is stopped without running its remaining code. Nothing waits for goroutines automatically. Block main until the work is done with a WaitGroup or a channel receive. Adding time.Sleep only hides the problem.