Lesson 8 of 12
Goroutines & Channels
Learn how to run work concurrently with goroutines and pass data between them with channels. Goroutines are cheap, so Go programs often start thousands of them.
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // signal completion no matter how we return
time.Sleep(100 * time.Millisecond)
fmt.Printf("worker %d finished\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // count BEFORE starting the goroutine
go worker(i, &wg) // "go" runs it concurrently
}
wg.Wait() // block until every Done() has been called
fmt.Println("all done")
// Without the WaitGroup, main would exit immediately and kill every
// goroutine mid-flight. Nothing waits for a goroutine automatically.
}package main
import "fmt"
func main() {
// Unbuffered channel: a send blocks until a receiver is ready.
done := make(chan string)
go func() { done <- "finished" }()
fmt.Println(<-done)
// Buffered channel: sends succeed until the buffer is full.
jobs := make(chan int, 100)
results := make(chan int, 100)
// Worker pool — three goroutines sharing one queue of work
for w := 1; w <= 3; w++ {
go func(id int) {
for j := range jobs { // ranges until jobs is closed
results <- j * j
}
}(w)
}
for i := 1; i <= 9; i++ {
jobs <- i
}
close(jobs) // tells the workers no more work is coming
total := 0
for i := 0; i < 9; i++ {
total += <-results
}
fmt.Println(total) // 285
// Direction in the signature documents intent and is compiler-checked
// func produce(out chan<- int) send-only
// func consume(in <-chan int) receive-only
}package main
import (
"context"
"errors"
"fmt"
"time"
)
// select waits on several channels at once
func race(a, b <-chan string) string {
select {
case msg := <-a:
return "a: " + msg
case msg := <-b:
return "b: " + msg
case <-time.After(2 * time.Second):
return "timeout"
}
}
// context is how you cancel work in Go: pass it as the FIRST parameter.
func fetch(ctx context.Context, url string) (string, error) {
select {
case <-time.After(500 * time.Millisecond): // pretend this is a network call
return "body of " + url, nil
case <-ctx.Done():
return "", ctx.Err() // context canceled / deadline exceeded
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel() // always cancel to release resources
if _, err := fetch(ctx, "/slow"); errors.Is(err, context.DeadlineExceeded) {
fmt.Println("gave up:", err)
}
// Mutex when a channel would be overkill (a shared counter, a cache)
// var mu sync.Mutex; mu.Lock(); defer mu.Unlock()
}Common question
Is a goroutine a thread?
A goroutine is a running concurrently, by the Go runtime across threads it manages. Channels are typed between goroutines: a send waits until a receiver is ready, which both hands over the value and syncs the two. select waits on several channels at once, and context carries cancellation down a chain of calls.
Design so each piece of data belongs to one goroutine and gets passed on rather than shared. Where sharing cannot be avoided, protect it with a mutex. Always know how each goroutine ends, and run your tests with -race, which finds the mistakes code review misses.
Source: Rob Pike: "Do not communicate by sharing ; instead, share memory by communicating."
Common mix-ups
Each of these sounds reasonable. Think about why it is wrong, then open the explanation.
A goroutine runs concurrently, so it must be an operating-system thread.
Channels are safe, so a program that uses them can't deadlock.
Starting a goroutine guarantees it will finish before the program exits.
Tip: use the ← and → keys to move between lessons.