Goroutines and Channels

Course 2 · Ch 3
Goroutines and Channels
Go's built-in answer to "do several things at once" — lightweight, and designed to talk to each other safely

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

func sayHello() { fmt.Println("Hello from a goroutine!") } func main() { go sayHello() // runs concurrently, doesn't block main() fmt.Println("Hello from main!") time.Sleep(100 * time.Millisecond) // give the goroutine time to actually run }

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() doesn't wait for goroutines automatically
If 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

func calculate(n int, results chan int) { results <- n * n // send n² into the channel } func main() { results := make(chan int) go calculate(5, results) value := <-results // receive — BLOCKS until something arrives fmt.Println(value) // 25 }

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

func square(n int, results chan int) { results <- n * n } func main() { numbers := []int{2, 3, 4} results := make(chan int, len(numbers)) // buffered channel for _, n := range numbers { go square(n, results) } for i := 0; i < len(numbers); i++ { fmt.Println(<-results) } } // 4, 9, 16 — order is NOT guaranteed

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

import "sync" func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // runs when worker() returns, however it returns fmt.Println("Worker", id, "done") } func main() { var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) go worker(i, &wg) } wg.Wait() // blocks until all 3 have called Done() fmt.Println("All workers finished") }

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 — run this just before the function returns
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.
JavaScriptGo
Single-threaded event loop, never truly parallelGoroutines 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 severalsync.WaitGroup — Add/Done/Wait
No raw threads exposed to the developergo keyword launches a real concurrent goroutine directly

Coding Challenges

Challenge 1

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 solution
Challenge 2

Given 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 solution
Challenge 3

Write 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 solution

Chapter 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.