Skip to content

Intermediate · Concurrency: goroutines, channels and context · topic 26 of 46 · 35 min

Goroutines and WaitGroup

Explanation

A goroutine is a function running at the same time as the rest of your program, started with the go keyword. Goroutines are very cheap, so a program can run thousands of them. When main returns, the program ends and all goroutines stop, so you wait for them with a sync.WaitGroup (or channels).

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.

Think of it like this: A busy dhaba kitchen: one cook (a core) can start the dal, put rotis on the tawa, and chop onions while things simmer. That is concurrency. Three cooks working at once is parallelism. Goroutines are the orders on the counter; the Go runtime is the head cook deciding who works on what.

Starting a goroutine

go
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"
}
Most likely prints only "main is done". When main returns, every goroutine is stopped.
Careful: Using time.Sleep in main to "wait for goroutines" is a bug waiting to happen. You never know how long they need. Use a WaitGroup or channels.

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

go
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")
}
The three SMS lines can come in any order; "all sent" always comes last. With go 1.22+ in go.mod, each goroutine gets its own p.
  • 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

go
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]
Writing to different elements of a slice from different goroutines is safe. Writing to the same variable or to a map is not (see the sync topic).
Tip: Before Go 1.22 you had to write go func(p string) { ... }(p) or p := p inside the loop, because all goroutines shared one loop variable. You will still see this in older code.

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

Goroutines

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.

go
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
}
Results keep the bank order because each goroutine writes its own index.

Practice (0/4 done)

Task 1 · Question

What does this most likely print? func main() { go fmt.Println("hello") }

Task 2 · Question

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()

Task 3 · Question

Three goroutines each print their number 1, 2, 3. What order do you see?

Task 4 · Practice

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

Question 1

1.How do you start a goroutine?

Question 2

2.What happens to running goroutines when main returns?

Question 3

3.What is the difference between concurrency and parallelism?

Question 4

4.What does GOMAXPROCS control?

Read more (free)

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.

From Concurrency interview questions and coding rounds

What is the Go proverb about concurrency?Related

Answer: Share memory by communicating

Pass data through channels instead of sharing variables where you can.

From Channels: passing data between goroutines

Which is best for a simple hit counter shared by many goroutines?Related

Answer: atomic.Int64

atomic.Int64 gives safe, fast Add and Load.

From Mutex, Once, atomic and the race detector

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.

From select: waiting on several channels

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

  1. 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.
  2. Module 6 exam: Concurrency · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Standard library essentials. 46 topics left in the course before the final exam.

← Previous: Generics: type parameters and constraints