Go
Goroutines & Channels
This is the feature Go is most associated with, and the one Introduction to Go named as the language's central design goal. A goroutine is an extremely cheap, independently-scheduled function call; a channel is a typed pipe two goroutines use to pass data safely, without touching a mutex directly.
- What it is
- A goroutine is a lightweight, concurrently-executing function, managed by Go's own runtime rather than the OS directly. A channel is a typed conduit for sending values between goroutines.
- Mental model
- Go's own slogan for it: "don't communicate by sharing memory; share memory by communicating" — pass data through a channel instead of having two threads both reach into the same shared variable.
- Key numbers
- A goroutine starts with only a few KB of stack (growing as needed) — genuinely cheap to spawn thousands of, unlike a full OS thread.
- Reach for it when
- Work can happen concurrently (multiple network requests, parallel processing) and you'd otherwise reach for OS threads plus manual locking in C/C++.
Goroutines
func sayHello() {
fmt.Println("Hello from a goroutine")
}
func main() {
go sayHello() // the "go" keyword — runs sayHello CONCURRENTLY, doesn't wait for it
fmt.Println("Hello from main")
time.Sleep(100 * time.Millisecond) // give the goroutine time to run before main exits
}
The go keyword is the entire mechanism — prefixing any function call with it launches that
call as a new goroutine, running concurrently with everything else. This is dramatically lighter-weight
than spawning an OS thread in C/C++, which is exactly why Go programs commonly run thousands of
goroutines at once without a second thought.
Channels
ch := make(chan int) // a channel that carries int values
go func() {
ch <- 42 // SEND 42 into the channel
}()
value := <-ch // RECEIVE from the channel — blocks until something is sent
fmt.Println(value) // 42
Sending and receiving both block by default until the other side is ready — this is the actual safety mechanism. Instead of two goroutines racing to read and write the same shared variable (exactly the hazard a mutex exists to prevent in C/C++), they hand a value through the channel, one at a time, with the language itself enforcing the handoff.
The producer/consumer pattern, visually
select — waiting on multiple channels
select {
case msg1 := <-channel1:
fmt.Println("received from channel1:", msg1)
case msg2 := <-channel2:
fmt.Println("received from channel2:", msg2)
case <-time.After(1 * time.Second):
fmt.Println("timed out waiting")
}
select waits on multiple channel operations at once, proceeding with whichever one becomes
ready first — Go's built-in tool for exactly the "wait on several things simultaneously, plus a timeout"
problem that's much more manual to build in C/C++.
Real-world applications
- Handling many simultaneous network connections — a Go web server commonly spawns one goroutine per incoming request, each handled independently and cheaply.
- Fan-out/fan-in processing — splitting work across several worker goroutines reading from a shared input channel, then collecting results on an output channel.
- Timeouts and cancellation —
selectwithtime.After(as above) is the idiomatic Go pattern for "do this, but give up after N seconds."
Gotchas
- A goroutine that blocks forever on a channel nobody ever sends to (or receives from) is a real, genuine deadlock — Go's runtime can detect and report the simplest case (every goroutine blocked), but not every possible deadlock shape.
- Launching a goroutine with
goand never synchronizing with it meansmaincan exit before it finishes — exactly what thetime.Sleepin the first example works around informally; real code uses async.WaitGroupor a channel to wait properly instead. - Goroutines are cheap, but not free — spawning genuinely unbounded numbers of them (one per item in an enormous, uncontrolled input) can still exhaust memory; a worker-pool pattern with a fixed number of goroutines is the common fix.
- Go's concurrency model doesn't eliminate every possible race condition — sharing a plain variable (not a channel) between goroutines without synchronization is still a real data race, exactly like the shared-memory hazard Embedded's Interrupts chapter warns about for ISR/main-loop shared state.