Go Concurrency Distilled
Go Concurrency Distilled
Authored by Anton Zhiyanov
This condensed guide offers a high-level summary of various concurrency patterns and primitives within the Go programming language.
Note: This resource is intended as a
beginner's tutorialquick refresher for those already familiar with the basics. If you are starting from scratch, please refer to Gist of Go: Concurrency.
🗺️ Roadmap of Topics
- Goroutines
- Channels
- Select
- Pipelines
- Time
- Context
- Wait groups
- Data races
- Race conditions
- Mutexes
- Semaphores
- Signaling
- Run once
- Object pool
- Atomics
- Testing
- Scheduling
- Diagnostics
- Final thoughts
🚀 Goroutines
The core of Go's concurrency model is the goroutine. A goroutine is a lightweight thread managed by the Go runtime, initiated simply by prefixing a function call with the go keyword.
How it Works
The Go runtime acts as an intermediary, multiplexing goroutines onto a smaller number of actual operating system (OS) threads.
Key Characteristics:
- Efficiency: Goroutines are significantly cheaper than OS threads. While an OS thread might require megabytes of stack space, a goroutine starts with a few kilobytes.
- Implicit Entry: The
mainfunction is itself a goroutine that triggers automatically upon program execution. - Resource Cost: .
Synchronization with sync.WaitGroup
Because the main goroutine does not automatically wait for others to finish, we use a sync.WaitGroup to prevent the program from exiting prematurely.
Add(n): Increases the internal counter by .Done(): Decreases the counter by .Wait(): Blocks the current goroutine until the counter hits .
func main() {
var wg sync.WaitGroup
wg.Add(2) // Expecting two workers
go worker(1, &wg)
go worker(2, &wg)
wg.Wait() // Block until both call Done()
}
Pro Tip: WaitGroup.Go is a helper that encapsulates the Add, execution, and Done steps into a single call.
📨 Channels
Channels serve as the communication pipes that allow goroutines to synchronize and exchange data. Think of a channel as a conveyor belt or a window where one party drops off a value and another picks it up.
Basic Communication
Sending (ch <- val) and receiving (val := <-ch) are synchronous (blocking) operations by default. The sender blocks until a receiver is ready, and vice versa.
func main() {
messages := make(chan string)
go func() {
messages <- "ping" // Blocks until main receives
}()
msg := <-messages // Blocks until goroutine sends
fmt.Println(msg) // Output: ping
}
The Output Channel Pattern
A frequent Go idiom involves a function creating a channel, launching a background goroutine to populate it, and returning the channel to the caller.
func generate(start, stop int) chan int {
out := make(chan int)
go func() {
for i := start; i < stop; i++ {
out <- i
}
}()
return out
}
Closing Channels
The close() function signals to receivers that no more data will be sent.
The "Comma OK" Idiom:
When reading from a channel, you can determine if it is still open using a second boolean return value:
val, ok := <-ch
ok == true: Value was received from an open channel.ok == false: Channel is closed and empty;valis the zero-value of the type.
Warning: Writing to a closed channel or closing a channel twice will trigger a panic.
You must always close every channel. Correction: Only close a channel if the receiver needs to know that the stream has ended. Otherwise, the Garbage Collector (GC) will handle it.
Iteration with range
The range keyword simplifies reading until a channel is closed:
for n := range nums {
fmt.Print(n, " ")
}
Note: Unlike slices, range over a channel returns only the value, not the index.
Directional Channels
To increase type safety and prevent logic errors, you can specify if a channel is only for sending or receiving.
| Syntax | Direction | Capability |
|---|---|---|
chan T | Bidirectional | Read and Write |
chan<- T | Send-only | Write only |
<-chan T | Receive-only | Read only |
Go automatically casts bidirectional channels to directional ones when passed into functions.
Buffered Channels
Buffered channels act as FIFO queues with a capacity .
- Non-blocking Write: Occurs as long as
len(ch) < cap(ch). - Non-blocking Read: Occurs as long as
len(ch) > 0.
stream := make(chan int, 3) // Buffer size of 3
stream <- 11
stream <- 12
stream <- 13
fmt.Println(<-stream) // 11
The nil Channel
A channel's zero value is nil.
- Reading/Writing to a
nilchannel Blocks forever. - Closing a
nilchannel Panic.
⚡ Select
The select statement is somewhat l... (Text ends here)