Intermediate · Concurrency: goroutines, channels and context · topic 26 of 46 · 35 min
Goroutines and WaitGroup
Explanation
Concurrency and parallelism
Concurrency means dealing with many tasks whose lifetimes overlap: while one waits for the network, another runs. Parallelism means literally running at the same instant on different CPU cores. Go makes concurrency easy; the Go runtime then spreads goroutines across cores, which gives parallelism when the machine has several cores.
Starting a goroutine
func sendSMS(to string) {
time.Sleep(100 * time.Millisecond) // pretend network call
fmt.Println("sent to", to)
}
func main() {
go sendSMS("98XXXXXX01") // runs concurrently
go sendSMS("98XXXXXX02")
fmt.Println("main is done")
// main returns here: the program exits, probably before any SMS is "sent"
}Why goroutines are cheap
- A goroutine starts with a small stack of a few kilobytes that grows only when needed. An operating-system thread usually reserves much more.
- The Go runtime has its own scheduler. It runs many goroutines on a small number of OS threads, switching between them when one blocks on I/O, a channel or a lock.
- GOMAXPROCS sets how many threads run Go code at the same time. It defaults to the number of CPU cores (and, since Go 1.25 on Linux, respects container CPU limits).
- So a web server can give every request its own goroutine. That is exactly what net/http does.
Waiting with sync.WaitGroup
func main() {
phones := []string{"98XXXXXX01", "98XXXXXX02", "98XXXXXX03"}
var wg sync.WaitGroup
for _, p := range phones {
wg.Add(1) // one more goroutine to wait for
go func() {
defer wg.Done() // this one is finished
sendSMS(p)
}()
}
wg.Wait() // block until the counter is zero
fmt.Println("all sent")
}- Call wg.Add(1) BEFORE the go statement, not inside the goroutine, or Wait might run before Add.
- Call wg.Done() with defer, so it runs even if the function returns early.
- Pass a WaitGroup by pointer if you hand it to another function; copying it breaks it (go vet warns).
- Go 1.25 added wg.Go(func() { ... }), which does Add, go and Done for you. Older code uses the three calls above.
Order is not guaranteed
Goroutines run in whatever order the scheduler picks. Never assume goroutine 1 finishes before goroutine 2. If order matters, collect results with their index and sort, or send them through channels (next topic).
Returning results
prices := []int{100, 250, 499}
results := make([]int, len(prices))
var wg sync.WaitGroup
for i, p := range prices {
wg.Add(1)
go func() {
defer wg.Done()
results[i] = p + p*18/100 // each goroutine writes its OWN index: safe
}()
}
wg.Wait()
fmt.Println(results) // [118 295 588]Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Goroutines and WaitGroup · 1/7
Concurrency is Go's superpower. It starts with goroutines.
Prefer a video by a teacher? Find free videos on “golang goroutines waitgroup tutorial concurrency vs parallelism” on YouTube ↗
In real life: Checking many bank APIs at once
A payment dashboard shows whether 5 partner bank APIs are up. Calling them one after another takes 5 × 300 ms = 1.5 seconds. With one goroutine per bank and a WaitGroup, all checks run at once and the page is ready in about 300 ms, the time of the slowest single call.
package main
import (
"fmt"
"sync"
"time"
)
func ping(bank string) string {
time.Sleep(300 * time.Millisecond) // pretend HTTP call
return bank + ": up"
}
func main() {
banks := []string{"BankA", "BankB", "BankC", "BankD", "BankE"}
status := make([]string, len(banks))
start := time.Now()
var wg sync.WaitGroup
for i, b := range banks {
wg.Add(1)
go func() {
defer wg.Done()
status[i] = ping(b)
}()
}
wg.Wait()
fmt.Println(status)
fmt.Println("took about", time.Since(start).Round(100*time.Millisecond)) // ~300ms
}Practice (0/4 done)
What does this most likely print? func main() { go fmt.Println("hello") }
Which line is the bug? 1: var wg sync.WaitGroup 2: for i := 0; i < 3; i++ { 3: go func() { 4: wg.Add(1) 5: defer wg.Done() 6: work(i) 7: }() 8: } 9: wg.Wait()
Three goroutines each print their number 1, 2, 3. What order do you see?
Write a program that "downloads" 5 files concurrently (each download sleeps for a random 100 to 500 ms using math/rand/v2) and stores how long each took in a slice. Wait with a WaitGroup, then print the slice and the total time, which should be close to the slowest single download.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Channels: passing data between goroutinesConcurrency: goroutines, channels and context · same module
- select: waiting on several channelsConcurrency: goroutines, channels and context · same module
- Mutex, Once, atomic and the race detectorConcurrency: goroutines, channels and context · same module
- Concurrency interview questions and coding roundsCareers and interviews
- ThreadsRust Programming · Concurrency and async
- Interview questions: traits, errors, pointers and concurrencyRust Programming · Rust careers and interviews
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
How do you start a goroutine?
Answer: go f()
The go keyword before a function call starts a goroutine.
What happens to running goroutines when main returns?
Answer: They are stopped because the program exits
The program ends when main returns.
What is the difference between concurrency and parallelism?
Answer: Concurrency is overlapping tasks; parallelism is running at the same instant on several cores
Go gives concurrency; the runtime provides parallelism on multi-core machines.
What does GOMAXPROCS control?
Answer: How many OS threads can run Go code at the same time
It defaults to the number of available CPU cores.
A goroutine panics without recover. What happens?Related
Answer: The whole program crashes
An unrecovered panic in any goroutine terminates the process.
What is the Go proverb about concurrency?Related
Answer: Share memory by communicating
Pass data through channels instead of sharing variables where you can.
Which is best for a simple hit counter shared by many goroutines?Related
Answer: atomic.Int64
atomic.Int64 gives safe, fast Add and Load.
What is a goroutine leak?Related
Answer: A goroutine blocked forever that never ends, holding memory
Leaked goroutines pile up and waste memory over time.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 4 more topics in “Concurrency: goroutines, channels and context” before its exam: Channels: passing data between goroutines, select: waiting on several channels, Mutex, Once, atomic and the race detector, context: timeouts and cancellation.
- Module 6 exam: Concurrency · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Standard library essentials. 46 topics left in the course before the final exam.