Go Concurrency Distilled
This article offers a concise exploration of key concurrency concepts in the Go programming language. Each topic is accompanied by interactive examples, allowing readers to experiment with the code and observe the outcomes by clicking "Run." A PDF version of the book is also available, featuring static examples.
Go concurrency is primarily based on goroutines, lightweight functions initiated with the `go` keyword. The Go runtime manages these goroutines and allocates them across operating system threads that run on CPU cores. Unlike OS threads, goroutines are extremely lightweight, enabling the creation of hundreds or even thousands of them.
A crucial aspect of goroutines is their independence. The `main` function, which is also a goroutine, begins implicitly when the program starts. Once the `main` function concludes, other goroutines will also terminate. To ensure goroutines complete their execution before the program exits, a wait group (`sync.WaitGroup`) can be employed.
This group maintains a counter, which is incremented by the `Add(n)` method and decremented by the `Done()` method. The `Wait()` method blocks the calling goroutine until the counter reaches zero, ensuring all workers have finished before the program ends.
Goroutines can communicate with each other using channels, which function as windows through which one goroutine can send values and another can receive them. Sending a value through a channel is synchronous, meaning the sending goroutine halts until another goroutine receives the value. When writing an output channel from a function and populating it within an internal goroutine, the caller can receive values through the channel while the owning function retains control.
To indicate to readers that all data has been sent, the writer goroutine closes the channel using `close()`.
If the channel is closed, readers receive a zero value and a false status when attempting to read. A channel can only be closed once; attempting to close it again or write to a closed channel results in a panic. However, closing a channel is only necessary to signal the readers that all data has been sent. If this information is not relevant to the readers, there is no need to close the channel.
Upon a channel's disuse, Go's garbage collector will release its resources, regardless of whether it has been closed or not. The `range` keyword allows for automatic reading of the next value from a channel and checks if it has been closed. When reading from a closed buffered channel, the values from the buffer are returned along with a true status. Upon exhausting all values, it returns a zero value and a false status, similar to a regular channel.
Channels in Go can be initialized for both reading and writing purposes. They are often specified as directional in function parameters. Buffered channels function as a FIFO queue with a fixed-size buffer for storing values. While the buffer has free space, writing to the channel does not block the goroutine. Similarly, as long as the buffer contains values, reading from the channel does not block the goroutine.
By default, if no buffer size is specified, a channel is unbuffered, with a buffer size equal to zero. Buffered channels can be checked for their length and capacity using the built-in `len()` and `cap()` functions.
Like any other type in Go, channels possess a zero value, which is `nil`. Writing to or reading from a nil channel causes the goroutine to block indefinitely. The `select` statement is akin to the `switch` statement but is specifically designed for channels. It allows for managing data flow in pipelines, where each step processes input data, transforms it, and outputs the result.
Channels serve as the input and output of each operation in a pipeline. A goroutine can signal other goroutines that it has finished its work using an output channel. In scenarios where a goroutine does not need to return results, it can signal completion via a done channel.
Error handling in concurrent pipelines can be approached in several ways. Besides handling time-related operations, the `time` package provides tools for managing time-sensitive operations in concurrent programs. The `time.After()` function returns a channel that is initially empty but receives a value after the specified timeout period, making it useful for timing out operations.
The `withTimeout()` function waits for a function to complete, but it also incorporates `time.After()` to ensure the operation does not exceed the timeout duration.
Timers, represented by the `time.Timer` structure, provide a channel that sends the current time when triggered (i.e., upon expiration). Timers are useful for scheduling future executions. The `Stop()` method stops the timer and returns true if it has not yet expired, while `false` indicates that the timer has already triggered. If a timer is employed within a loop, it is more efficient to create a single timer and reset it instead of instantiating a new timer on each iteration.
A ticker functions similarly to a timer, periodically sending the current time to a channel at a specified interval.
Written by urgent.news from Hacker News's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.
This story
This is one outlet's version. Read the fullest account.
- Go concurrency distilled antonz.org