跳转至

goroutine、channel 与 select:Go 的并发模型,连同它的两种经典事故

这一章回答四个问题: 怎么开一条并发执行流(goroutine)? 两条流之间怎么安全地传数据(channel)?怎么同时等好几件事(select、WaitGroup)? 以及——并发程序会出哪两种经典事故(数据竞争、死锁),怎么自己把它们造出来再修好? 读完你会拿到 Go 并发模型的完整心智图,外加「race 是这样产生的、锁是这样挡的」的亲手经验。

1. 这一章在原书里的位置

原书第 3 章(Go Concurrency)是语言部分的压轴。第一部分开篇说过一个立场, 这一章是它的兑现:并发在 Go 里不是优化手段,而是思维模型1。 书里那句口号值得现在记住,后面全章都是它的展开: 「不要通过共享内存来通信,要通过通信来共享内存。」2

2. 顶层全景

  ① 执行单元   goroutine    → go 关键字;调度归运行时,不归操作系统(§3)
  ② 等待机制   WaitGroup    → Add/Done/Wait 计数;Go 1.25 起有 wg.Go(§4)
  ③ 沟通管道   channel      → 无缓冲=当面交接;缓冲=信箱;关频道有规矩(§5–§6)
  ④ 多路监听   select       → 哪个 channel 先就绪走哪个;配 time.After 做超时(§7)
  ⑤ 两个事故   race/deadlock → 亲手制造、工具抓获、设计根治(§8–§11)

图说:五个部件一张图。①是「谁干活」,③④是「怎么配合」,⑤是「配合坏了什么样」。

3. goroutine:开一条执行流,只需要一个 go

goroutine 是 Go 里独立的执行流。 它不是什么重型的「线程」: 你在函数调用前加一个 go 关键字,这个调用就被调度到一条 goroutine 上异步跑, 当前代码不等它3。背后的事(先给效果,细节在第 4 章调度器一节): 运行时管理一个线程池,用「偷工作」的调度算法把成千上万个 goroutine 复用到少量 操作系统线程上;同一时刻在跑 Go 代码的线程数上限由 GOMAXPROCS 决定 (默认值是机器的逻辑 CPU 数),阻塞在系统调用上的 goroutine 不占这个名额4。

每个 Go 程序至少有一条 goroutine——跑 main() 的那条。 想让程序整体退出要用 os.Exit();goroutine 自己结束用 return 就够了5。

第一个怪现象:main 不等任何人

书上第一个程序就故意留下一个坑:main 里循环 go worker(i) 开出 20 条 goroutine, 没有任何等待机制,然后 main 直接结束。输出什么样?6

  $ go run ch03/goroutines.go 20
  Starting 20 goroutines...
  Worker 3 starting
  Main function is exiting without waiting!
  Worker 20 starting
  Worker 1 starting
  (然后程序就没了——剩下 17 条 goroutine 根本没轮到执行)

图说:主函数一退出,整个进程(连同所有 goroutine)立刻蒸发。 输出的顺序每次还不一样——goroutine 何时被调度,你说了不算。

这个例子还带一个语言级的坑(书里用 Note 专门警告): goroutine 按引用捕获循环变量,不按值拷一份。 循环里 go func(){ 用 i }() 拿到的 i 是循环那个变量的内存位置,等 goroutine 真跑起来时循环可能已经走到后面了—— 经典症状是「所有 goroutine 打印同一个数」。修法曾经是循环开头写 i := i 造一个局部副本; Go 1.22 起编译器自动为每轮迭代造新变量,这个坑被官方填了7。

什么时候该开 goroutine:幂等性判据

书里给了一个不常见但好用的判据:一个操作如果做多次结果一样(幂等,idempotent), 它就是 goroutine 的好候选——因为幂等意味着不依赖执行过程中会变化的外部状态8。 反过来,如果一条 goroutine 的结果依赖某个全局变量此刻的值,它产出的就是非确定性。

4. sync.WaitGroup:等大家都干完的计数器

解决「main 不等人」的标准工具是 sync.WaitGroup,一个计数器,三个方法:

var wg sync.WaitGroup
for _, t := range tasks {          // tasks = task1, task2, task3
    wg.Add(1)                        // ① 计数 +1
    go func(task string) {
        defer wg.Done()              // ② 干完计数 −1;defer 保证任何退出路径都执行
        fmt.Println("Processing:", task)
    }(t)
}
wg.Wait()                            // ③ 计数不归零就一直等
fmt.Println("All tasks completed!")  // 这句永远最后打印

跑出来的顺序可能每次不同(Processing: task2 在 task1 前面), 但 All tasks completed! 永远最后——这就是 WaitGroup 的合同9。 两个纪律:每个 Add() 必须有配对的 Done();Add 多了,Wait() 永等(死锁的一种); Done 多了,计数变负,直接 panic「negative WaitGroup counter」10。

Go 1.25 加了 wg.Go(func(){...}):把「+1、开 goroutine、defer Done」三步合一, 少掉忘写 Done 的整个 bug 类别11。本章后面的 worker pool 和 monitor 例子都用了它。

5. channel:goroutine 之间的管道

goroutine 之间不能直接通信,共享数据只有两条路:channel,或者(尽量避免的)带锁的全局变量12。 channel 两个要素:类型(只能传这一种值)和缓冲(能暂存几个值)。

  • 无缓冲 ch := make(chan int):每一次发送都要等一个接收,反之亦然——当面交接, 双方必须同时就位。这是严格的同步点。
  • 缓冲 ch := make(chan int, 3):发送方最多领先 3 个值不等人,第 4 个开始才阻塞; 接收方只在空了的时候阻塞。

书上 bufUnbuf.go 的输出把这个差别演得极清楚(发送每个值花 200ms,接收花 400ms)13:

  无缓冲:Sending 1... Sent 1! Received 1  Sending 2...    ← 严格交替,发送必须等接收
  缓冲:  Sending 1... Sent 1! Received 1  Sending 2... Sent 2!
         Received 2  Sending 3... Sent 3! Sending 4... Sent 4!  ← 发送连跑4个不等
         Received 3  ← 直到缓冲塞满,才再次出现等待

图说:无缓冲是「手递手」,缓冲是「信箱」。缓冲大小的选择就是「生产方能领先消费方多少」的选择, 这也是背压(backpressure)的最原始形态。

关闭 channel 的规矩

三条,违反就是事故:

  1. 只能由发送方关闭,且确定不再发了才关;
  2. 往已关闭的 channel 发送 → 运行时 panic("send on closed channel");
  3. 从已关闭的 channel 接收 → 立即返回零值和 ok=false,所以 for v := range ch 循环能在 channel 关闭时自动退出——「收完即走」靠的就是这条14。

6. 主线例子(一):发送者—接收者邮箱,完整走一遍

sendReceive.go 是本章的机制主线:一个 sender 往 channel 放 1–5,一个 receiver 取, 取完各自报平安。我们按时间线走,旁边是程序真实输出的交错顺序15:

  t=0     sender:Sending 1     → 无缓冲,handshake 等 receiver
  t≈0     receiver:received 1  → sender 放行,睡 300ms;receiver 睡 500ms(更慢)
  t≈300   sender:Sending 2     → 又一次 handshake
  t≈500   receiver:received 1(处理完)  → 收下 2
  ……(节奏由两边的 sleep 决定,channel 自动对表)
  t=1500  sender:Sending 5 → received 5
  t=1800  sender:closing channel(close(ch))
  t=2000  receiver:range 循环感知关闭 → 退出循环 → 往 done channel 发 true
  main    <-done 收到 → 打印 Main: all done!

关键机制每一步都在场:无缓冲 channel 的发送阻塞(对表点)、 for num := range ch 的收完即走、第二个 channel done 作为完成信号。 main 用 <-done 等 receiver——注意这又是一个「主函数等 worker」的姿势, 和第 4 节的 WaitGroup 互为替代。

7. select:同时监听多条线,配超时

select 像 switch,但分支是 channel 操作:所有分支同时被考察,哪个先就绪走哪个; 多个同时就绪,运行时随机挑一个;全都没就绪就阻塞;select{} 永远阻塞16。

书上 select.go 的例子:两个 worker 各睡 0–1000ms 随机时长,谁先醒谁的结果先被收:

select {
case msg1 := <-ch1:  fmt.Println("Received:", msg1)
case msg2 := <-ch2:  fmt.Println("Received:", msg2)
}

某次运行:Received: Worker 2 finished after 53ms 先于 Worker 1 finished after 270ms—— 顺序由随机睡眠决定,不由代码书写顺序决定17。 代码里还有个类型细节:ch chan<- string(参数里箭头的位置)表示「只能发的 channel」, 把「只能读」写错成「往里写」会在编译期报错——channel 方向的类型检查18。

超时:给无限等待装个闸门

生产环境的真理(原话):「无限期的等待和故障没有区别。」19 给 select 加一个 case <-time.After(2 * time.Second): 分支,2 秒内没人来就走超时路。 演示:shortTask 睡 1 秒、longTask 睡 5 秒,都设 2 秒超时—— 输出 Short: Short task finished 和 Long: Timeout! Took too long.20。

8. 主线例子(二):亲手制造一次数据竞争,再用工具抓获

raceF1.go:全局变量 counter,5 条 goroutine 各 counter++ 1000 次, WaitGroup 保证都干完再打印。期望值 5 × 1000 = 5000。实际呢?21

  Final counter value (expected 5000): 3282     ← 某次运行

少了 1718 次。 为什么?counter++ 看着是一行,实际是三步:读出、加一、写回。 两条 goroutine 同时做这件事,交叠是这样的(时间从上往下):

  goroutine A           goroutine B            counter
  读出 1000                                    1000
                        读出 1000                1000
  加一得 1001
                        加一得 1001
  写回 1001                                    1001   ← B 的那一次加一丢了!
                        写回 1001

图说:两次加一只生效了一次——竞争(race)不是「结果随机」,是「更新无声地丢失」。 而且 WaitGroup 只管「等齐」,管不了「别抢」——同步和互斥是两件事22。

抓获:go run -race

$ go run -race ch03/raceF1.go
WARNING: DATA RACE
Read at 0x000104f2b0d0 by goroutine 8:
  main.main.func1()  raceF1.go:21 +0x88
Previous write at 0x000104f2b0d0 by goroutine 9:  ...
Final counter value (expected 5000): 4000
Found 2 data race(s)
exit status 66

race detector 的原理(书里有):给二进制插桩,监视运行期每一次内存访问, 发现「多 goroutine 同时访问同一变量、且至少一个是写」就报, 附冲突双方的调用栈23。代价是变慢、费内存——所以它是开发和测试工具,不是生产配置。

修复:mutex,以及一个 defer 的权衡

var mu sync.Mutex
// 每次 ++ 改成:
mu.Lock()
counter++
mu.Unlock()

mutex(互斥锁)保证同一时刻只有持锁者能进临界区——改完预期 5000 稳稳拿到24。 配套知识三条:

  • sync.RWMutex:读远多于写时用——读者可以多人同时持锁,写者独占。 写频繁时它比 Mutex 没有优势25。
  • defer mu.Unlock() 是默认纪律,防早退路径漏解锁;但锁住的函数别太长—— defer 到函数返回才解锁,长函数等于长时间占锁。短函数 defer,长函数尽早手动 Unlock26。
  • 忘掉 Unlock = 另一类死锁:程序不崩,就是不动(挂起)。

9. 主线例子(三):亲手制造一次死锁,再看运行时怎么报

deadlock.go:无缓冲 channel,worker 想发 42,main 也想发 99,没有任何人接收:

  worker goroutine: ch <- 42   ← 阻塞,等接收
  main goroutine:   ch <- 99   ← 也阻塞,等接收
  → fatal error: all goroutines are asleep - deadlock!
    goroutine 1 [chan send]:   main.main()  deadlock.go:22
    goroutine 35 [chan send]:  main.main.func1()  deadlock.go:15

图说:两条 goroutine 互相等待一个永远不会来的条件,围成环。 Go 运行时会自动侦测(全部 goroutine 都睡着、无人能醒来时 panic 并报出每条卡在哪), 这是第 ① 个例子的黑暗镜像——那次是 main 不等人跑了,这次是都等别人谁也不动。 修复简单到一句话:一发必须配一收;把 main 改成 val := <-ch,输出立刻正常27。

10. 设计根治:矩阵乘法的「各写各格」

第 8 节用锁挡住竞争,第 10 节展示更高的境界:结构设计让竞争不可能发生。 矩阵乘法 result[i][j] = A 的第 i 行 · B 的第 j 列(点积,m 次乘加)。 书上的并发方案:给结果矩阵的每个格子开一条 goroutine——

for i := 0; i < n; i++ {
    for j := 0; j < k; j++ {
        wg.Go(func() {          // 每条 goroutine 算一个格子
            sum := 0
            for l := 0; l < m; l++ { sum += A[i][l] * B[l][j] }
            result[i][j] = sum   // Safe: each goroutine writes to a unique cell
        })
    }
}

没有锁、没有 channel,却没有竞争——因为每条 goroutine 只写自己的格子, 写集两两不相交28。书上就着这个例子说了一句全章的题眼: 「先仔细想程序结构,再考虑要不要 mutex、channel、select。」 顺带一个真实输出(2×3 乘 3×2):[0 1 -1] × [[0 0],[0 1],[-1 0]] = [1 1]—— 手算一格验证:第 1 行第 1 列 = 0×0 + 1×0 + (−1)×(−1) = 1,吻合29。

11. 两个并发模式:worker pool 与 monitor

worker pool(workerPool.go):固定 N 条 worker 从共享 jobs channel 抢活, 结果写 results channel;干完的条件链是「jobs 发完即 close → worker 的 range 退出 → WaitGroup 归零 → close(results)→ main 的 range 退出」。 输出里 job 的完成顺序天然乱序(2 个 worker 处理 5 个 job)30。 书里的选择判据值得带走:资源受限(内存大、API 限流)用池子限并发;轻量任务直接开 goroutine, 池子是多余的复杂31。

monitor goroutine(monitor.go):一条 goroutine 独占一个计数器, 所有人通过 channel 发请求(请求里内嵌一条 reply channel 拿回执):

type Request struct { Increment int; Reply chan int }
// monitor:count += req.Increment;req.Reply <- count

3 个 worker 各加 2 次,输出里「谁看到哪个数」乱序(Worker 0 看到 6 先于 Worker 1 看到 5), 但计数器本身 1,2,3,4,5,6 一个不少——状态只有一个人在改,竞争从结构上不存在32。 这就是那句口号的最完整演示:不共享内存,只通信,而共享自然发生。

12. 作者的判断与证据

判断 性质 依据
通信优先于共享内存 Go 官方哲学,作者奉为纲 口号原文 + monitor 例子是全书最贴的演示232
goroutine 要用幂等性把关 作者自己的经验法则 「A critical rule of thumb」原段8
channel 只能由发送方关闭 语言规则 + 社区铁律 send on closed channel 会 panic 的机制说明14
结构设计优先于锁 作者明确的工程主张 矩阵乘法一节的原话题眼28
-race 是开发期必备、不上生产 工具事实 + 作者建议 插桩原理与开销说明23
worker pool vs 直接开 goroutine 按资源约束选 作者的取舍判据 章内 Note31

13. 边界与局限

  • sync/atomic 完全没讲。 计数器这个例子最贴的修法其实是原子操作 (atomic.AddInt64),比 mutex 快一个量级;书中只给了 mutex 一条路。
  • context 取消没进本章。 超时只教了 time.After;「上游喊停、下游级联退出」这套 生产环境的核心机制,要等第 9 章 context 一节。
  • channel 内存模型(formal memory model)没讲。 「happens-before」这类保证 一句未提,读者得自己相信「channel 传数据顺带同步」。
  • race detector 的覆盖盲区没讲:它只抓「同一进程内」的交叠;跨进程的共享 (数据库、文件)它看不见。
  • goroutine 泄漏只是超时一节暗示了一下(leaking resources), 没有作为独立题目教——第 13 章的「goroutine leak profile」(Go 1.26)才回来补。

14. 可带走的

  1. go 关键字开 goroutine;main 一退全部蒸发——等齐用 WaitGroup,一对一报善用 done channel。
  2. WaitGroup 三件套 Add/Done/Wait;Go 1.25 的 wg.Go() 把三步合一,优先用它。
  3. 循环变量按引用捕获;Go 1.22 起 for range 每轮自动换新变量,旧代码里的 i := i 不再必要。
  4. 无缓冲 channel = 当面交接(强同步);缓冲 channel = 信箱(capacity 是领先额度)。
  5. 关闭规矩:发送方关、只关一次、往关闭的 channel 发会 panic、range 靠「收完即走」退出。
  6. select 同时监听多个 channel;加 time.After 分支 = 超时;无限等待 = 故障。
  7. race 的机理是「读-加-写」三步交叠丢更新;go run -race 抓现行;短临界区用 defer Unlock,长临界区别。
  8. 死锁 = 全员睡着围成环;运行时会自动报;一发配一收是不二法门。
  9. 矩阵乘法式的设计根治:让写集不相交,比锁更可靠——先想结构,再上锁。
  10. 共享状态交给一条 monitor goroutine,请求里内嵌 reply channel——Go 哲学的完整形态。

15. 原文地图

主题 原书章 原文位置
并发是思维模型、章节立场 Learning about goroutines text/34-ch03.txt:6(搜「not as an optimization」)
「通过通信共享内存」口号 Learning about goroutines text/35-fm-learning-about-goroutines.txt:32(搜「do not communicate」)
GOMAXPROCS 与线程池 Learning about goroutines text/35-fm-learning-about-goroutines.txt:23(搜「GOMAXPROCS」)
main 不等人的第一个例子 Learning about goroutines text/35-fm-learning-about-goroutines.txt:109(搜「Starting 20 goroutines」) · :124(搜「remaining 17 goroutines」)
循环变量按引用捕获、Go 1.22 修复 Learning about goroutines text/35-fm-learning-about-goroutines.txt:84(搜「by reference」) · :90(搜「variable shadowing」)
幂等性判据 Learning about goroutines text/35-fm-learning-about-goroutines.txt:224(搜「idempotency」)
WaitGroup 用法与失衡后果 Learning about goroutines text/35-fm-learning-about-goroutines.txt:150(搜「sync.WaitGroup」) · :192(搜「crucial that every call」)
WaitGroup.Go(Go 1.25) Learning about goroutines text/35-fm-learning-about-goroutines.txt:245(搜「new in Go 1.25」)
channel 关闭规矩 Learning about channels text/36-fm-learning-about-channels.txt:16(搜「once closed」)
select 语义 Learning about channels text/36-fm-learning-about-channels.txt:30(搜「select keyword」) · :44(搜「examined simultaneously」)
邮箱主线例子(sendReceive) Learning about channels text/36-fm-learning-about-channels.txt:153(搜「func sender」) · :212(搜「Sender: sending 1」)
缓冲 vs 无缓冲输出对比 Learning about channels text/36-fm-learning-about-channels.txt:338(搜「bufUnbuf.go」) · :371(搜「fundamental difference」)
超时与「无限等待=故障」 Learning about channels text/36-fm-learning-about-channels.txt:402(搜「indistinguishable from a failure」) · :477(搜「Timeout! Took too long」)
worker pool 与选择判据 Learning about channels text/36-fm-learning-about-channels.txt:494(搜「worker pool」) · :668(搜「resource constraints」) · :640(搜「2 workers for 5 jobs」)
monitor goroutine Learning about channels text/36-fm-learning-about-channels.txt:749(搜「Monitor goroutines」) · :776(搜「type Request struct」) · :835(搜「Worker 0 sees counter」)
竞争制造与 -race 抓获 Catching deadlocks and race conditions text/37-fm-catching-deadlocks-and-race-conditions.txt:65(搜「counter++」) · :85(搜「expected 5000」) · :93(搜「WARNING: DATA RACE」)
mutex 修复、RWMutex、defer 解锁权衡 Catching deadlocks and race conditions text/37-fm-catching-deadlocks-and-race-conditions.txt:179(搜「mu.Lock」) · :205(搜「RWMutex」) · :221(搜「defer mu.Unlock」)
死锁制造与运行时侦测 Catching deadlocks and race conditions text/37-fm-catching-deadlocks-and-race-conditions.txt:434(搜「blocks forever」) · :467(搜「all goroutines are asleep」)
矩阵乘法各写各格 Catching deadlocks and race conditions text/37-fm-catching-deadlocks-and-race-conditions.txt:367(搜「unique cell」) · :390(搜「go run ch03/matrix.go」)


  1. 出处:「Go Concurrency」第 5–8 段(text/34-ch03.txt:6,搜「not as an optimization」)。「we must embrace concurrency not as an optimization but as a model of thought」。 ↩

  2. 出处:「Learning about goroutines」第 29–38 段(text/35-fm-learning-about-goroutines.txt:32,搜「do not communicate by sharing memory」)。这是 Go 社区的官方口号,CSP 模型(通信顺序进程)的根子书里未展开,属通用知识。 ↩↩

  3. 出处:「Learning about goroutines」第 40–45 段(text/35-fm-learning-about-goroutines.txt:41,搜「go keyword」)。「When you prefix a function or method call with the go keyword, Go schedules that call to run asynchronously」。 ↩

  4. 出处:「Learning about goroutines」第 20–28 段(text/35-fm-learning-about-goroutines.txt:23,搜「GOMAXPROCS」)。原文:运行时可以多开线程处理阻塞的系统调用,但同时跑 Go 代码的线程数不超过 GOMAXPROCS。 ↩

  5. 出处:「Learning about goroutines」第 11–15 段(text/35-fm-learning-about-goroutines.txt:13,搜「os.Exit」)。 ↩

  6. 出处:「Learning about goroutines」(text/35-fm-learning-about-goroutines.txt:109,搜「Starting 20 goroutines」;:111,搜「exiting without waiting」;:124,搜「remaining 17 goroutines」)。 ↩

  7. 出处:「Learning about goroutines」Note(text/35-fm-learning-about-goroutines.txt:84,搜「by reference」;:90,搜「variable shadowing」;:91,搜「Go 1.22」)。 ↩

  8. 出处:「Learning about goroutines」Note(text/35-fm-learning-about-goroutines.txt:220,搜「overuse」;:224,搜「idempotency」)。「A critical rule of thumb… is the concept of idempotency」。 ↩↩

  9. 出处:「Learning about goroutines」(text/35-fm-learning-about-goroutines.txt:150,搜「sync.WaitGroup」;:204,搜「Processing: task2」;:216,搜「always printed last」)。 ↩

  10. 出处:「Learning about goroutines」第 192–200 段(text/35-fm-learning-about-goroutines.txt:192,搜「crucial」)。Add 5 次 Done 4 次的失衡示例与 negative counter panic 都在同段。 ↩

  11. 出处:「Learning about goroutines」第 245–253 段(text/35-fm-learning-about-goroutines.txt:245,搜「new in Go 1.25」)。「combining the counter increment, goroutine launch, and deferred Done() call into a single step」。 ↩

  12. 出处:「Learning about goroutines」第 29–38 段(text/35-fm-learning-about-goroutines.txt:29,搜「cannot communicate with each other directly」)。「there is no shared memory in the traditional way… unless we are dealing with global variables」。 ↩

  13. 出处:「Learning about channels」(text/36-fm-learning-about-channels.txt:338,搜「bufUnbuf.go」;:377,搜「strict alternating」;:384,搜「racing ahead」)。两段输出分别在第 338–369 行。 ↩

  14. 出处:「Learning about channels」第 16–23 段(text/36-fm-learning-about-channels.txt:16,搜「once closed」)。「if you write to a closed channel, Go panics at runtime with the "send on closed channel" error」;接收得零值 + ok=false 在同段。 ↩↩

  15. 出处:「Learning about channels」(text/36-fm-learning-about-channels.txt:153,搜「func sender」;:212,搜「Sender: sending 1」;:223,搜「Sender: closing channel」)。完整交错输出见第 212–225 行。 ↩

  16. 出处:「Learning about channels」第 29–49 段(text/36-fm-learning-about-channels.txt:36,搜「select{}」;:44,搜「examined simultaneously」;:48,搜「random selection」)。 ↩

  17. 出处:「Learning about channels」(text/36-fm-learning-about-channels.txt:124,搜「select.go」;:125,搜「Worker 2 finished after 53ms」)。 ↩

  18. 出处:「Learning about channels」第 106–109 段(text/36-fm-learning-about-channels.txt:107,搜「chan<- string」)。「it can only send data, not receive it – this is a safety feature」。 ↩

  19. 出处:「Learning about channels」第 401–403 段(text/36-fm-learning-about-channels.txt:402,搜「indistinguishable from a failure」)。 ↩

  20. 出处:「Learning about channels」(text/36-fm-learning-about-channels.txt:448,搜「case result := <-shortDone」;:477,搜「Timeout! Took too long」)。 ↩

  21. 出处:「Catching deadlocks and race conditions」(text/37-fm-catching-deadlocks-and-race-conditions.txt:65,搜「counter++」;:85,搜「expected 5000」)。3282 是该次运行值,原文注明「typically prints a value less than 5,000」。 ↩

  22. 出处:「Catching deadlocks and race conditions」第 73–90 段(text/37-fm-catching-deadlocks-and-race-conditions.txt:80,搜「does not prevent the race itself」)。「sync.WaitGroup ensures that the main() function waits… but it does not prevent the race itself」。 ↩

  23. 出处:「Catching deadlocks and race conditions」第 20–36 段(text/37-fm-catching-deadlocks-and-race-conditions.txt:23,搜「-race flag」)。「instruments the binary and monitors all memory accesses during execution」;开销与退出码 66 见第 27–30、141 行。 ↩↩

  24. 出处:「Catching deadlocks and race conditions」(text/37-fm-catching-deadlocks-and-race-conditions.txt:179,搜「mu.Lock」)。修复版代码与「reliably matches the expected 5,000」见第 187–204 段。 ↩

  25. 出处:「Catching deadlocks and race conditions」第 205–213 段(text/37-fm-catching-deadlocks-and-race-conditions.txt:205,搜「RWMutex」)。「allows multiple readers to hold the lock simultaneously」。 ↩

  26. 出处:「Catching deadlocks and race conditions」第 214–233 段(text/37-fm-catching-deadlocks-and-race-conditions.txt:221,搜「defer mu.Unlock」;:224,搜「relatively short」)。长短函数的权衡在同段。 ↩

  27. 出处:「Catching deadlocks and race conditions」(text/37-fm-catching-deadlocks-and-race-conditions.txt:434,搜「blocks forever」;:467,搜「all goroutines are asleep」;:504,搜「val := <-ch」;:529,搜「Main: received 42」)。 ↩

  28. 出处:「Catching deadlocks and race conditions」(text/37-fm-catching-deadlocks-and-race-conditions.txt:367,搜「unique cell」;:260,搜「think carefully」)。「Safe: each goroutine writes to a unique cell」是代码注释原文。 ↩↩

  29. 出处:「Catching deadlocks and race conditions」(text/37-fm-catching-deadlocks-and-race-conditions.txt:390,搜「go run ch03/matrix.go」)。输出与矩阵维度见第 390–400 行。 ↩

  30. 出处:「Learning about channels」(text/36-fm-learning-about-channels.txt:641,搜「2 workers for 5 jobs」;:594,搜「wg.Go」)。jobs/results 双 channel 与关闭链见第 589–618 行。 ↩

  31. 出处:「Learning about channels」Note(text/36-fm-learning-about-channels.txt:671,搜「resource constraints」)。「opt for a worker pool when dealing with heavy resource constraints」。 ↩↩

  32. 出处:「Learning about channels」(text/36-fm-learning-about-channels.txt:749,搜「Monitor goroutines」;:776,搜「type Request struct」;:835,搜「Worker 0 sees counter」;:848,搜「1, then 2」)。 ↩↩