Go concurrency distilled

Go Concurrency Distilled

Go 并发精要

This mini-book provides a brief overview of many concurrency topics in Go. Each topic comes with interactive examples — feel free to experiment with them by changing the code and clicking Run. There’s also a PDF version with static examples. 这本小书简要概述了 Go 语言中的许多并发主题。每个主题都附带交互式示例——你可以随意修改代码并点击“运行”进行尝试。此外,还有一个包含静态示例的 PDF 版本。

This is a quick refresher on Go concurrency, not a beginner’s guide. If you want to learn concurrency from the ground up with practical exercises, check out my other book — Gist of Go: Concurrency. 这是一份关于 Go 并发的快速复习指南,而非入门教程。如果你想从零开始并通过实践练习学习并发,请查看我的另一本书——《Gist of Go: Concurrency》。

The book is AI-free. 本书内容由人工编写,未使用 AI 生成。

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 • 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

Goroutines(协程)

The foundation of concurrency in Go is goroutines – functions started with the go keyword: Go 并发的基础是 goroutines——即通过 go 关键字启动的函数:

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
        defer wg.Done()
        fmt.Println("worker 1")
    }()
    go func() {
        defer wg.Done()
        fmt.Println("worker 2")
    }()
    wg.Wait()
}
// worker 2
// worker 1

The Go runtime juggles these goroutines and distributes them among operating system threads running on CPU cores. Compared to OS threads, goroutines are lightweight, so you can create hundreds or thousands of them. Go 运行时负责调度这些 goroutines,并将它们分配给运行在 CPU 核心上的操作系统线程。与操作系统线程相比,goroutines 非常轻量,因此你可以创建成百上千个。

Goroutines are completely independent. The main function is also a goroutine, but it starts implicitly when the program starts. When main ends, other goroutines also shut down. Goroutines 是完全独立的。main 函数本身也是一个 goroutine,它在程序启动时隐式启动。当 main 结束时,其他 goroutines 也会随之关闭。

We use a wait group (sync.WaitGroup) to wait for goroutines to finish in the example above. A wait group has a counter inside. Calling Add(n) increments it by n, while Done() decrements it by one. Wait() blocks the calling goroutine (in this case, main) until the counter reaches zero. This way, main waits for both workers to finish before it exits. 在上面的示例中,我们使用等待组 (sync.WaitGroup) 来等待 goroutines 完成。等待组内部有一个计数器。调用 Add(n) 会将其增加 n,而 Done() 会将其减一。Wait() 会阻塞调用它的 goroutine(在本例中为 main),直到计数器归零。这样,main 就会在退出前等待两个工作协程完成。

Channels

Channels(通道)

Goroutines can pass values to each other through channels. A channel is like a window where one goroutine can throw something and another can catch it: Goroutines 可以通过通道相互传递值。通道就像一个窗口,一个 goroutine 可以从这里扔出东西,另一个则可以接住它:

func main() {
    messages := make(chan string)
    go func() {
        messages <- "ping"
    }()
    msg := <-messages
    fmt.Println(msg)
}
// ping

Sending a value through a channel is a synchronous operation. When the sending goroutine writes a value to the channel (ch <- val), it blocks and waits for someone to receive that value (<-ch). Only then does it continue. 通过通道发送值是一个同步操作。当发送方的 goroutine 向通道写入值 (ch <- val) 时,它会阻塞并等待接收方接收该值 (<-ch)。只有在那之后,它才会继续执行。

Output channel

输出通道

Returning an output channel from a function and filling it within an internal goroutine is a common pattern in Go. This allows the caller to receive values through the channel while the owning function retains control of it: 从函数返回一个输出通道并在内部 goroutine 中填充它,是 Go 中的一种常见模式。这允许调用者通过通道接收值,同时拥有该通道的函数保留对其的控制权:

func generate(start, stop int) chan int {
    out := make(chan int)
    go func() {
        for i := start; i < stop; i++ {
            out <- i
        }
    }()
    return out
}

Closing a channel

关闭通道

To signal readers that all data has been sent, the writer goroutine closes the channel with close(): 为了通知读取者所有数据已发送完毕,写入方的 goroutine 会使用 close() 关闭通道:

func generate(start, stop int) chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for i := start; i < stop; i++ {
            out <- i
        }
    }()
    return out
}

The reader checks the channel’s status with a second value (“comma OK”) when reading: 读取者在读取时可以通过第二个返回值(“逗号 OK”惯用法)来检查通道的状态:

func main() {
    in := generate(5, 10)
    for {
        num, ok := <-in
        if !ok {
            break
        }
        fmt.Print(num, " ")
    }
}
// 5 6 7 8 9

While the channel is open, the reader receives the next value and a true status. If the channel is closed, the reader gets a zero value and a false status. 当通道打开时,读取者会接收到下一个值以及 true 状态。如果通道已关闭,读取者会得到零值以及 false 状态。

A channel can only be closed once. Closing it again or writing to a closed channel causes a panic. 通道只能关闭一次。再次关闭或向已关闭的通道写入数据会导致程序崩溃(panic)。

The only reason to close a channel is to signal to its readers that all data has been sent. If this isn’t important to the readers, then you don’t need to close it. When a channel is no longer used, Go’s garbage collector will free its resources, whether it’s closed or not. 关闭通道的唯一理由是通知读取者所有数据已发送完毕。如果这对读取者不重要,则无需关闭它。当通道不再被使用时,无论是否关闭,Go 的垃圾回收器都会释放其资源。

Channel iteration

通道迭代

range automatically reads the next value from the channel and checks if it’s closed. If the channel is closed, it exits the loop: range 会自动从通道读取下一个值并检查它是否已关闭。如果通道已关闭,它会退出循环:

func main() {
    nums := generate(5, 10)
    for n := range nums {
        fmt.Print(n, " ")
    }
}
// 5 6 7 8 9

Range over a channel returns a single value, not a pair, unlike range over a slice. 与对切片使用 range 不同,对通道使用 range 只返回单个值,而不是键值对。

Directional channels

定向通道

You can protect yourself from accidental write/close errors by setting the channel direction. Channels can be: 你可以通过设置通道方向来防止意外的写入/关闭错误。通道可以是:

  • chan (bidirectional): for reading and writing (default);
  • chan(双向):用于读写(默认);
  • chan<- (send-only): for writing only;
  • chan<-(只发):仅用于写入;
  • <-chan (receive-only): for reading only.
  • <-chan(只收):仅用于读取。

You can’t read from a send-only channel or write to a receive-only channel (nor can you close it). 你不能从只发通道读取数据,也不能向只收通道写入数据(也不能关闭它)。

Channels are usually initialized for both reading and writing, and specified as directional in function parameters. Go automatically converts a regular channel to a directional one: 通道通常在初始化时是双向的,但在函数参数中会被指定为定向通道。Go 会自动将常规通道转换为定向通道:

stream := make(chan int)
go func(in chan<- int) {
    in <- 42
}(stream)
func(out <-chan int) {
    fmt.Println(<-out)
}(stream)
// 42

Buffered channels

带缓冲通道

Buffered channels work like a FIFO queue with a fixed-size buffer for storing values. 带缓冲通道的工作方式类似于一个具有固定大小缓冲区的 FIFO(先进先出)队列,用于存储值。

As long as the buffer has free space, writing to the channel doesn’t block the goroutine. Similarly, as long as the buffer contains values, reading from the channel doesn’t block the goroutine: 只要缓冲区有空闲空间,向通道写入数据就不会阻塞 goroutine。同样,只要缓冲区包含值,从通道读取数据也不会阻塞 goroutine:

stream := make(chan int, 3)
stream <- 11
stream <- 12
stream <- 13
fmt.Println(<-stream)
fmt.Println(<-stream)
// 11
// 12

By default, if you don’t specify a buffer size, a channel is unbuffered (buffer size equals zero). 默认情况下,如果你不指定缓冲区大小,通道就是无缓冲的(缓冲区大小为零)。

Buffered channels work with the built-in len() and cap() functions: 带缓冲通道可以与内置的 len() 和 cap() 函数配合使用:

stream := make(chan int, 3)
stream <- 11
fmt.Println(cap(stream), len(stream))
// 3 1

Reading from a closed buffered channel returns values from the buffer and a true status. Once all values are taken, it returns a zero value and a false status, like a regular channel: 从已关闭的带缓冲通道读取数据会返回缓冲区中的值以及 true 状态。一旦所有值都被取走,它就会像常规通道一样返回零值和 false 状态:

stream := make(chan int, 1)
stream <- 11
close(stream)
val, ok := <-stream // 11 true
val, ok = <-stream  // 0 false

nil channel

nil 通道

Like any type in Go, channels have a zero value, which is nil. 像 Go 中的任何类型一样,通道的零值是 nil。

Writing to or reading from a nil channel blocks the goroutine forever. 向 nil 通道写入或从中读取数据会永久阻塞该 goroutine。