Goroutines
Prerequisites: Generics
A goroutine is a function running concurrently with the rest of your
program. Start one by putting go in front of a function call. Goroutines are
managed by the Go runtime, not the operating system; they start with a small
stack, so a program can run thousands of them.
Two facts matter from the start:
- When
mainreturns, the program exits — running goroutines are killed. You must explicitly wait for work you care about. - The scheduler decides when each goroutine runs. You cannot assume any order between them unless you synchronise.
Syntax
go doWork(42)
go func() {
fmt.Println("anonymous goroutine")
}()
The problem: main doesn't wait
package main
import "fmt"
func main() {
go fmt.Println("from goroutine")
fmt.Println("from main")
}
This output is not deterministic. Usually you see only from main,
because main returns before the goroutine gets a chance to run.
Example: waiting with sync.WaitGroup
A sync.WaitGroup counts outstanding goroutines: Add(1) before starting
one, Done() when it finishes, and Wait() blocks until the count is zero.
(The sync lesson covers it in detail.)
package main
import (
"fmt"
"sync"
"time"
)
func slowSquare(n int) int {
time.Sleep(100 * time.Millisecond) // pretend this is slow work
return n * n
}
func main() {
nums := []int{1, 2, 3, 4, 5}
results := make([]int, len(nums))
start := time.Now()
var wg sync.WaitGroup
for i, n := range nums {
wg.Add(1)
go func() {
defer wg.Done()
results[i] = slowSquare(n) // each goroutine writes only its own index
}()
}
wg.Wait()
fmt.Println(results)
fmt.Println("elapsed:", time.Since(start).Round(100*time.Millisecond))
}
Expected output
[1 4 9 16 25]
elapsed: 100ms
The five 100 ms tasks ran concurrently, so the total is roughly 100 ms rather than 500 ms (the exact elapsed time varies slightly between runs). The results are in order because each goroutine writes to its own slice index — the goroutines themselves may finish in any order.
Because Go 1.22+ creates new i and n variables for every loop iteration,
each closure sees its own values.
Common mistakes
- Letting
mainreturn before goroutines finish, or "fixing" it withtime.Sleep. Sleeping is a guess; use aWaitGroupor channels. - Having several goroutines write the same variable (for example
total += n) without synchronisation. That is a data race: the result is unpredictable. Rungo run -race .orgo test -raceto detect it. - Calling
wg.Add(1)inside the goroutine.Waitmay run before theAddand return too early. CallAddbefore thegostatement. - Starting goroutines that can never finish (a goroutine leak). Every goroutine you start should have a clear way to end.
Practice
Split nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} into three chunks of three.
Sum each chunk in its own goroutine, storing the result in sums[i], wait
for all of them, then print sums and the grand total.
Show solution
package main
import (
"fmt"
"sync"
)
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
const chunks = 3
size := len(nums) / chunks
sums := make([]int, chunks)
var wg sync.WaitGroup
for i := range chunks {
wg.Add(1)
go func() {
defer wg.Done()
for _, n := range nums[i*size : (i+1)*size] {
sums[i] += n // only this goroutine touches sums[i]
}
}()
}
wg.Wait()
total := 0
for _, s := range sums {
total += s
}
fmt.Println(sums, total) // [6 15 24] 45
}