Goroutines

Concurrency is Go's superpower, and goroutines are how you reach it. By the end of this lesson you'll start work running in the background with one keyword, wait for it correctly with a WaitGroup , avoid the classic loop-variable bug, and protect shared data with a Mutex .

Learn Goroutines in our free Go course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick recall.

Part of the free Go course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

Goroutines are not OS threads. They start at about 2 KB of stack and are multiplexed onto a small pool of threads by the Go runtime, so launching thousands is normal and cheap.

1️⃣ Starting a Goroutine

Put go in front of any function call and it runs concurrently — your code keeps going without waiting for it. The catch every beginner hits: main is itself a goroutine, and when main returns the whole program exits, abandoning any goroutines still running. Below we pause with time.Sleep just to see them — that's a teaching crutch we'll replace next.

2️⃣ Waiting Properly with WaitGroup

Sleeping is fragile — too short and you miss output, too long and you waste time. The correct tool is sync.WaitGroup : a counter you bump with Add(1) before each goroutine, decrement with Done() when it finishes, and block on with Wait() . Always call Done through defer so it runs even if the goroutine returns early.

Your turn. Add the two missing WaitGroup calls so the program waits for all three jobs before printing the final line.

3️⃣ The Loop-Variable Trap

A famous gotcha: starting goroutines inside a loop that all reference the loop variable. In older Go versions every goroutine could end up seeing the same final value because they shared one variable. The bulletproof fix — correct in every Go version — is to pass the loop variable as an argument , giving each goroutine its own copy.

4️⃣ Protecting Shared State with Mutex

When many goroutines read and write the same variable at once, you have a data race — the result is unpredictable and undefined. A sync.Mutex fixes it: Lock() lets exactly one goroutine into the critical section, and Unlock() lets the next in. Run code with go run -race to have Go detect races for you.

These lines run three goroutines and wait for them with a WaitGroup . Put them in the correct order:

C, G, D, E, A, F, H, B. Declare the WaitGroup, then loop: inside the loop Add(1) first, then start the goroutine whose body defers Done . Close the goroutine literal and the loop, then Wait() after the loop. Add must run before the goroutine starts so the counter is never raced.

Predict the output before revealing the answer.

Usually nothing. main returns immediately and the program exits before the goroutine gets a chance to run. There's no Wait or Sleep to hold the program open.

Some number ≤ 1000, unpredictable — this is a data race because counter++ is unprotected. Wrap it in mu.Lock() / mu.Unlock() (or use atomic ) to get a reliable 1000.

Deadlocks. The counter is 1 and nothing ever calls Done , so Wait blocks forever. Go detects this and panics with "all goroutines are asleep - deadlock!".

Q: How is a goroutine different from a thread?

A goroutine is much lighter — it starts at roughly 2 KB and the Go runtime schedules many goroutines onto a small set of OS threads. You can run hundreds of thousands of goroutines; you could not run hundreds of thousands of OS threads.

Concurrency makes no promise about ordering. Goroutines are scheduled independently, so two consecutive runs can print in different orders. If you need ordering, coordinate with channels or a WaitGroup .

Only when goroutines share mutable state. If each goroutine works on its own data, or you pass results through channels, you may need no lock at all. Locks are for the shared-variable case.

Q: When should I prefer channels over a WaitGroup?

Use a WaitGroup when you only need to wait for completion. Use channels when goroutines need to send data back or coordinate work. You'll learn channels in the next lesson.

No blanks — just a brief. Launch one goroutine per name, wait for them all with a WaitGroup , and remember to pass the name in as an argument. This is the canonical "fan out, then wait" pattern.

Practice quiz

How do you start a goroutine?

  • async f()
  • spawn f()
  • go f()
  • thread f()

Answer: go f(). Put go before a function call to run it concurrently as a goroutine.

What happens to running goroutines when main returns?

  • the program exits and abandons them
  • they keep running
  • main waits for them
  • they restart

Answer: the program exits and abandons them. main is itself a goroutine; when it returns the whole program exits, killing the rest.

What is the correct tool to wait for a set of goroutines?

  • time.Sleep
  • a for loop
  • sync.Mutex
  • sync.WaitGroup

Answer: sync.WaitGroup. A sync.WaitGroup with Add/Done/Wait waits reliably, unlike the fragile time.Sleep.

On a WaitGroup, which call blocks until the counter reaches zero?

  • Add
  • Wait
  • Done
  • Lock

Answer: Wait. Wait() blocks until every Add has a matching Done and the counter hits zero.

Why call wg.Done() with defer?

  • it runs even if the goroutine returns early
  • it runs faster
  • it adds to the counter
  • it locks the group

Answer: it runs even if the goroutine returns early. defer wg.Done() guarantees the decrement on every exit path of the goroutine.

wg.Add(1) should be called...

  • inside the goroutine
  • after Wait
  • before starting the goroutine
  • only once total

Answer: before starting the goroutine. Add before launching the goroutine so Wait can't race ahead and return too early.

What is the bulletproof fix for the loop-variable capture bug?

  • use a global
  • pass the loop variable as an argument to the goroutine
  • add a Sleep
  • use a Mutex

Answer: pass the loop variable as an argument to the goroutine. Passing the loop variable gives each goroutine its own copy in every Go version.

What happens when many goroutines write the same variable without a lock?

  • nothing, it's safe
  • a compile error
  • automatic locking
  • a data race with unpredictable results

Answer: a data race with unpredictable results. Unsynchronized shared writes are a data race; the result is undefined.

What does sync.Mutex provide?

  • a wait counter
  • one goroutine in the critical section at a time
  • a channel
  • goroutine scheduling

Answer: one goroutine in the critical section at a time. Lock() admits exactly one goroutine; Unlock() lets the next in, protecting shared state.

Which flag makes Go detect data races at runtime?

  • -vet
  • -async
  • -race
  • -lock

Answer: -race. Run with go run -race (or go test -race) to have Go report data races.