Goroutines and Channels
Fundamentals Chapter 10 covered JavaScript's async/await — useful for waiting on one thing at a time, like a single network request, without ever running two pieces of JavaScript truly simultaneously. Go's concurrency is fundamentally different: a Go program can run genuinely many things at once, using goroutines, and channels to let them communicate safely.
Starting a Goroutine with go
The keyword go placed before any function call launches it as a goroutine — a lightweight, independently-running unit of execution — and immediately continues with the next line, without waiting for it to finish. This is similar in spirit to JavaScript's "doesn't block" behaviour from Chapter 10's setTimeout, but a goroutine genuinely can run in parallel with everything else, on multi-core hardware.
main() returns before a goroutine finishes, the goroutine is simply abandoned — there is no equivalent of JavaScript's event loop keeping things alive until they're done. The time.Sleep above is a crude workaround used only for this introductory example; real code uses the synchronization tools below instead.
Channels — Sending Values Between Goroutines
make(chan int) creates a channel — a typed pipe that goroutines use to send and receive values safely. results <- n*n sends a value in; <-results receives one out. Receiving from a channel blocks — the receiving code waits patiently until a value actually arrives, which is exactly what removes the need for time.Sleep guesswork from the first example.
Running Several Goroutines and Collecting Results
make(chan int, len(numbers)) creates a buffered channel that can hold several values without anything having to receive immediately — letting all three goroutines send without waiting on each other. The receiving loop then collects exactly as many results as goroutines were launched. The order they arrive in is not guaranteed — whichever square finishes first sends first.
sync.WaitGroup — Waiting for a Group of Goroutines
WaitGroup is the real-world tool for "wait until everything launched is finished" — Add(1) registers one more goroutine to wait for, each goroutine calls Done() when finished, and Wait() blocks until the count returns to zero. defer wg.Done() guarantees Done() runs no matter how the function exits, even if it returns early or panics.
defer schedules a statement to run right before its surrounding function exits, regardless of which return path is taken. It's used constantly alongside resources that need cleanup (closing a file, unlocking something) — wg.Done() here is the most common beginner-facing example.
| JavaScript | Go |
|---|---|
| Single-threaded event loop, never truly parallel | Goroutines genuinely run in parallel on multi-core hardware |
| await / .then() to wait for a result | <-channel blocks until a value arrives |
| Promise.all([...]) to wait for several | sync.WaitGroup — Add/Done/Wait |
| No raw threads exposed to the developer | go keyword launches a real concurrent goroutine directly |
Coding Challenges
Write a function cube(n int, results chan int) that sends n³ into results. In main, create an unbuffered channel, launch cube(4, results) as a goroutine, receive the value, and print it.
📄 View solutionGiven numbers := []int{1, 2, 3, 4, 5}, launch a goroutine per number that sends its square into a buffered channel sized to len(numbers). Receive all 5 results in a loop, summing them into a total printed at the end.
📄 View solutionWrite a function worker(id int, wg *sync.WaitGroup) that prints "Worker started" and "Worker finished" with the id, using defer wg.Done(). Launch 4 workers using a sync.WaitGroup, then print "All done" only after wg.Wait() returns.
📄 View solutionChapter 3 Quick Reference
- go functionCall() — launches a goroutine; continues immediately without waiting
- make(chan Type) — unbuffered channel; sending/receiving blocks until matched
- make(chan Type, n) — buffered channel; can hold up to n values without blocking
- channel <- value — send; <-channel — receive (blocks until a value arrives)
- sync.WaitGroup — Add(n) to register, Done() when finished, Wait() to block until all are done
- defer statement — runs just before the surrounding function returns, however it returns
- main() does NOT wait for goroutines automatically — use channels or WaitGroup, not Sleep, in real code
- This completes this Go course's planned chapters. Advanced topics (generics, context, testing) would follow in a Course 3.