Concurrency
By the end of this lesson you'll be able to run work concurrently with goroutines, pass data safely between them with channels, wait on several operations with select , and coordinate everything with a WaitGroup and a Mutex — the foundation of every fast Go program.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
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
1️⃣ Goroutines: go func()
A goroutine is a function running concurrently with the rest of your program — like hiring a worker who goes off and does a job while you carry on. You start one by putting the keyword go in front of a function call. They are extremely cheap (around 2KB each), so running thousands is normal. The catch: when main returns, the program exits and any unfinished goroutines are killed — so you need a way to wait for them. That's what a sync.WaitGroup does: Add counts goroutines up, each one calls Done when finished, and Wait blocks until the count is zero.
Notice the three greetings can print in any order — the scheduler decides who runs when. Only all greetings done is guaranteed last, because wg.Wait() holds main back until every goroutine has finished.
2️⃣ Channels: send and receive
A channel is a typed pipe that goroutines use to pass values to each other safely — the conveyor belt from the analogy. You send with ch value and receive with value := ch (the arrow always points the way the data moves). Channels come in two flavours. An unbuffered channel ( make(chan T) ) has no storage, so a send blocks until a receiver is ready — that makes it a synchronisation point as well as a pipe. A buffered channel ( make(chan T, n) ) holds up to n values, so sends only block once it's full.
Your turn. The program below pings a "server" goroutine and waits for a reply. Fill in the two blanks marked ___ using the hints, then run it.
3️⃣ select : wait on many channels
select is like a switch for channels: it waits on several channel operations at once and runs whichever becomes ready first. This is how you add timeouts (race a real result against time.After ) and how you merge inputs from multiple goroutines. Add a default case and select becomes non-blocking — if nothing is ready right now, it runs default instead of waiting.
4️⃣ sync.WaitGroup and sync.Mutex
When several goroutines touch the same variable, you have a problem: count++ is actually read-add-write, and two goroutines can interleave and lose an update. That's a data race , and the result is wrong and unpredictable. A sync.Mutex (mutual-exclusion lock) fixes it: Lock lets only one goroutine through at a time and Unlock releases it for the next. Pair it with a WaitGroup to wait for all the goroutines to finish.
Now you wire up the WaitGroup yourself. Fill in the three blanks — Add before launching, Done when finishing, and Wait at the end:
5️⃣ Putting It Together: a Pipeline
Here's the philosophy in action. Instead of many goroutines fighting over one shared slice, each stage owns its data and passes it to the next stage through a channel — generate → square → consume . Notice the return type : a receive-only channel, so callers can read results but can't accidentally send into it. No mutex needed, because nothing is shared.
📋 Quick Reference
No blanks this time — just a brief and an outline. Launch five goroutines that each add their number to a shared total, protected by a mutex, and wait for them all with a WaitGroup. Run it and check your output against the expected line in the comments.
Practice quiz
How do you start a function running concurrently as a goroutine?
- async myFunc()
- concurrent myFunc()
- go myFunc()
- spawn myFunc()
Answer: go myFunc(). Putting the keyword go in front of a call launches it as a goroutine and returns immediately.
What does wg.Wait() do on a sync.WaitGroup?
- Blocks until the counter reaches zero
- Adds one to the counter
- Sleeps one second
- Resets the group
Answer: Blocks until the counter reaches zero. Wait blocks until every Add has been matched by a Done, i.e. the counter is back to zero.
Why can the order of goroutine output vary between runs?
- The compiler shuffles it
- Channels reorder values
- It is a bug in Go
- The scheduler decides when each goroutine runs
Answer: The scheduler decides when each goroutine runs. Goroutines run concurrently and the Go scheduler chooses when each one runs, so output order is not fixed.
What problem does a sync.Mutex solve?
- Starting goroutines faster
- A data race when goroutines update shared state
- Closing channels
- Buffering values
Answer: A data race when goroutines update shared state. A mutex serialises access so only one goroutine touches the shared variable at a time, preventing lost updates.
Where is the idiomatic place to put defer wg.Done() in a goroutine?
- As the first line of the goroutine
- As the last line
- Inside wg.Wait()
- After wg.Add
Answer: As the first line of the goroutine. Making defer wg.Done() the first line ensures it fires even if the function returns early or panics.
What is the difference between make(chan int) and make(chan int, 3)?
- No difference
- The first is faster
- The first is unbuffered; the second buffers up to 3
- The second cannot be closed
Answer: The first is unbuffered; the second buffers up to 3. make(chan int) is unbuffered (synchronises send/receive); make(chan int, 3) buffers up to three values.
What does a default case make a select statement do?
- Loop forever
- Become non-blocking — run default if nothing is ready
- Block until any case fires
- Close the channels
Answer: Become non-blocking — run default if nothing is ready. With a default case, select runs default immediately when no other case is ready, making it non-blocking.
What commonly causes 'fatal error: all goroutines are asleep - deadlock!'?
- Too many goroutines
- Using a Mutex
- Closing a channel twice
- A send on an unbuffered channel with no receiver
Answer: A send on an unbuffered channel with no receiver. A send (or receive) on an unbuffered channel with nothing on the other end blocks every goroutine — a deadlock.
In a pipeline, what does a return type of <-chan int mean?
- A buffered channel
- A receive-only channel
- A send-only channel
- A closed channel
Answer: A receive-only channel. <-chan int is a receive-only channel, so callers can read results but cannot accidentally send into it.
Why might a program exit before its goroutines print anything?
- Goroutines are too slow
- Channels block main
- When main returns the program exits and kills running goroutines
- The scheduler stops early
Answer: When main returns the program exits and kills running goroutines. When main returns the whole program exits, killing unfinished goroutines. Make main wait with a WaitGroup or channel.