Goroutines & Channels
Goroutines
A goroutine is a lightweight thread managed by the Go runtime. You start one by prefixing a function call with go:
Goroutines are extremely cheap โ you can spawn thousands without issue. The key point: if main returns, all goroutines are killed immediately.
Channels
Channels are typed conduits that let goroutines communicate safely. Create one with make:
Send a value with ch <- value, and receive with value := <-ch. Channels synchronize the sender and receiver โ both block until the other side is ready.
This example is complete and runnable: the main goroutine blocks on <-ch until sum sends its result.
Buffered Channels
A buffered channel accepts sends without a matching receiver โ up to its capacity:
Unbuffered channels block on every send until a receiver is ready. Buffered channels only block when the buffer is full (send) or empty (receive).
The select Statement
select is like a switch for channel operations โ it waits for whichever case is ready first:
If multiple cases are ready simultaneously, Go picks one at random. The default case runs immediately if no channel is ready, avoiding a block.
The Default Case in select
A default case makes channel operations non-blocking โ instead of waiting for a channel, Go immediately takes the default branch when no case is ready.
Try-receive pattern โ check if a value is available without blocking:
Try-send pattern โ drop a value rather than block when the channel is full:
Polling loop โ default combined with time.Sleep lets a goroutine periodically check a channel without ever blocking on it:
Typical use cases: rate limiting, circuit breakers, and checking whether a goroutine has finished without stalling the caller.
Range and Close
Closing a channel
close(ch) signals to receivers that no more values will be sent. Only the sender should close a channel โ closing a nil channel or closing one that is already closed will panic.
The comma-ok idiom
A receive expression returns a second boolean that tells you whether the channel is still open:
for range over a channel
for range handles the comma-ok check internally and stops the loop automatically when the channel is closed and empty:
This is the standard producer/consumer pattern: the producer goroutine closes the channel when it is done; the consumer uses for range rather than counting expected values.
Rules to avoid panics:
- Never close a channel from the receiver side
- Never close a channel more than once
- If multiple goroutines may send, coordinate closure with a
sync.WaitGroupand a dedicated closer goroutine
sync.WaitGroup
time.Sleep is a poor way to wait for goroutines. The idiomatic solution is sync.WaitGroup:
wg.Add(n)โ increment the counter by n before launching goroutineswg.Done()โ decrement the counter (always use withdefer)wg.Wait()โ block until the counter reaches zero
Channel Directions
Function signatures can constrain a channel to send-only or receive-only, preventing accidental misuse:
| Syntax | Meaning |
|---|---|
chan T | Bidirectional (read and write) |
chan<- T | Send-only |
<-chan T | Receive-only |
Directional channels improve safety and serve as documentation โ the signature tells you exactly how the channel is used.
sync.Mutex
When multiple goroutines share mutable state, protect it with a sync.Mutex:
The defer c.mu.Unlock() pattern ensures the mutex is always released, even if the function returns early or panics.
Goroutine Leaks
A goroutine that is never able to complete is a goroutine leak โ it consumes memory and CPU indefinitely. The most common cause is a goroutine blocked on a channel receive when no sender will ever send:
To prevent leaks: always ensure goroutines have a way to exit โ use a done channel, a context.Context cancellation, or close the channel when work is complete.
Knowledge Check
In a channel type `chan<- int`, what does `chan<-` mean?
What does a `select` statement do?
What happens when a select statement has a default case and no channel is ready?