跳转至

Go 并发 — goroutine、channel 与同步的一整张地图

这一章讲什么: Go 并发机制的全部家底——goroutine 和 channel 怎么工作、 有缓冲和无缓冲差在哪、七种协作模式、共享内存怎么用锁保住、竞态怎么抓、 性能怎么调。 它在全书链条里的位置: 第 01 章把并发列为 Go 的三大招牌之一,这一章把招牌 拆开给你看构造。第 08 章(并发流水线)是本章模式的工程化,第 10 章的性能调优 是本章 profiling 的生产版。 主线例子: 从「1000 个 goroutine 抢一个计数器」这个会算错的程序出发, 逐节修好它,最后长成一条流水线。每一步旁边都有具体的数和状态。

1. 顶层全景:CSP 模型的一张图

Go 的并发模型师承 CSP(Communicating Sequential Processes,通信顺序进程—— 一个并发计算理论,核心主张:并发单元之间靠传消息协作,而不是抢共享内存)1。

  goroutine ──► channel ──► goroutine ──► channel ──► goroutine
  (轻量执行单位)  (传送带)      (worker)     (传送带)     (收集结果)
      │                        │                         │
      └──── sync 包:万不得已要共享内存时,用锁保住临界区 ────┘
      └──── select:同时等好几条 channel,谁先来处理谁

四样原语(最基础、不可再拆的操作单元)的分工:go 起执行、channel 传数据、sync 保共享、select 多路等待; context 管取消(第 06 章专讲)2。

主线例子的起点(原书的例子): 期望输出 1000,实际输出每次都不一样:

var counter = 0
func main() {
    for i := 0; i < 1000; i++ {
        go func() {
            counter++ // unsafe
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(counter)
}

(在慢机器上它可能恰好打出 1000——「跑对了」不代表没竞态,这是并发 bug 最阴险之处。3)

2. goroutine:轻在哪,谁在调度

解决什么问题: 操作系统线程(OS thread)开一份就要兆字节(MB)级的栈; 一台服务器同时处理几万连接,线程模型要么开不起,要么开得起但调度开销吃掉一半 CPU。

goroutine 的做法: 启动时栈只有几千字节(KB 级),不够用再动态长、 不用了再缩回——所以一个进程里开成千上万乃至百万个goroutine 是常态4。 调度由 Go 运行时自己干,模型叫 G-M-P5:

字母 是什么
G goroutine 本身(一件待干的活)
M 操作系统线程(真正干活的劳力)
P 逻辑处理器,管着一条待运行队列

多个 G 复用少数 M;某个 P 的队列空了,去别的 P 那里偷活干(work stealing); 跑得太久的 G 会被异步抢占——不会一个死循环拖垮全家5。 goroutine 也可能主动挂起(等 channel、等锁、等 I/O),挂起时调度器把 CPU 让给别人5。

创建: go doWork() 一行;也可以起匿名函数的:go func(){ ... }()6。

主线例子第 ① 个 bug:循环变量捕获

for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i)   // 五个 goroutine 可能全打 5!
    }()
}

为什么错: 闭包捕获的是变量 i 本身,不是当时的值。循环结束时 i 已是 5, goroutine 真正运行时读到的就是那个 57。 修法(原书的惯用法): 把值当参数传进去——参数是拷贝:

for i := 0; i < 5; i++ {
    go func(v int) {
        fmt.Println(v)
    }(i)
}

(注:新版本 Go 已修改循环变量语义、每轮一个新变量,这个坑正在消失;但读懂它 才能看懂旧代码和 AI 给出的老写法。7)

3. channel:有缓冲与无缓冲,差的是「背压」

3.1 无缓冲:每一次传递都是一次握手

ch := make(chan string)——缓冲为零。发送和接收必须同时到场: 发送方把值递出去的那一瞬间就被停住,直到有人接;接收方没东西收时也停住,直到有人发。 原书比喻:接力交棒,两人必须在同一时刻碰到棒8。

ch := make(chan string)
go func() { ch <- "task done" }()   // 停在这里,直到有人接
msg := <-ch                          // 停在这里,直到有人发
fmt.Println(msg)

用途:严格同步、完成信号、「确保那件事干完了才继续」8。

3.2 有缓冲:一条有容量的队列

ch := make(chan int, 5)——能暂存 5 个值。缓冲区没满,发送不阻塞; 满了才变回同步行为9。生产快、消费慢时,缓冲吸收波动; 但缓冲越大,问题被掩盖得越久——消费者卡死,生产者还能若无其事地塞, 直到塞满那一刻大家一起 deadlock9。

3.3 背压(backpressure):系统的「别塞了」

背压指消费跟不上时,系统天然地拖慢生产。无缓冲 channel 提供即时背压 (每发必等);有缓冲的把背压推迟到缓冲塞满10。 选哪个,看 correctness 和 throughput 谁更要紧:要保护内存和下游系统 → 无缓冲或 小缓冲;要吞吐 → 大缓冲10。原书还拆了四个常见误解,值得各记一句11:

误解 实情
有缓冲总更快 大缓冲会掩盖失衡和死循环,内存用量不可预测
无缓冲太慢 需要同步时它反而省掉缓冲管理的开销
有缓冲就不用锁了 只在数据「全走 channel」时成立;共享内存照旧要锁
死锁只发生在无缓冲 缓冲满了、没人收,一样死锁

4. 七个协作模式(主线例子在这里长成流水线)

模式一:生产者–消费者。 生产者造任务投进 channel,消费者 range 取出处理; 生产者 close(channel),消费者的 range 自动结束12:

tasks := make(chan int, 5)
go func() { for i := 1; i <= 10; i++ { tasks <- i }; close(tasks) }()
go func() { for t := range tasks { fmt.Println("Processing", t) } }()

模式二:流水线(pipeline)。 每个环节一个 goroutine:收上游 channel、 变换、发下游 channel、close 下游通知收工。第 08 章把它工程化,这里记住形状: stage1 → stage2 → stage3,环节之间只靠 channel 说话13。

模式三:扇出/扇入(fan-out / fan-in)。 扇出:起 N 个 worker 同抢一个 jobs channel; 扇入:把 N 个 worker 的结果收拢到一个 results channel。主线例子的计数器修好后就长这样 (见 §6)14。

模式四:令牌通道(token channel)——用 channel 当信号量。 一个容量为 3 的缓冲 channel 里放 3 个令牌;goroutine 干活前取一个、干完还一个——同一时刻只有 3 个 在跑,不用一行锁15:

sem := make(chan struct{}, 3)
for i := 0; i < 10; i++ {
    go func(id int) {
        sem <- struct{}{}        // 取令牌;拿不到就等
        fmt.Println("Running job", id)
        time.Sleep(time.Second)
        <-sem                    // 还令牌
    }(i)
}

模式五:信号通知。 done := make(chan struct{})——不传数据,传「发生了」这个事实: 发一个空结构体、接收方等它。优雅退出(模式七)和跨 goroutine 的「等我干完」都用它16。

模式六:消灭共享——地图管理员 goroutine。 与其给 map 加锁,不如让一个 goroutine 独占这个 map,其他人想读写就发消息问它(查询里附带一个用于收答案的 reply channel)。 数据所有权在 goroutine 之间手递手转移,竞态从结构上消失。这就是常说的 「不要通过共享内存来通信,要通过通信来共享内存」17。

模式七:优雅退出。 专门的 quit channel;每个 goroutine 的主循环用 select 一边干活一边听 quit,close(quit) 全员有序收工18:

select {
case <-quit:
    return
default:
    // 正常干活
}

4.8 两个进阶原语走一遍:sync.Cond 与 sync.Once

表格太抽象,这两个值得各看一个真例子。

sync.Cond:有界队列的「等货/进货」信号。 场景:一个 goroutine 往队列放数据, 一个往外取;取的时候队列可能空,空了就睡,不占 CPU,等放的人叫醒—— 这就是条件变量(含义:一个「条件可能成立了」的广播站,睡着的 goroutine 挂上去等,条件成立的人负责广播):

var (
    mu   sync.Mutex
    cond = sync.NewCond(&mu)   // 条件变量必须配一把锁
    data = make([]int, 0)
)

func producer() {
    mu.Lock()
    data = append(data, 42)
    cond.Signal()              // 叫醒一个正在等的消费者
    mu.Unlock()
}

func consumer() {
    mu.Lock()
    for len(data) == 0 {       // 必须用 for 不是 if:醒来要再查一遍条件
        cond.Wait()            // 睡着;Wait 内部会临时放锁
    }
    v := data[0]
    data = data[1:]
    mu.Unlock()
    fmt.Println("Consumed:", v)
}

原书点名的坑:漏信号(Signal 发在 Wait 之前,消费者错过)、死锁(忘了在 锁内 Wait)、假唤醒(所以醒来必须再查条件,for 不能省)。这三个坑每一个 都是生产事故的形状,Cond 也因此成了 sync 包里最需要敬畏的一个。

sync.Once:懒初始化的保险。 有些初始化很贵(建连接、加载大表),想等第一次 真用到再做、且只做一次——sync.Once 的 Do(func(){...}) 保证即使在并发下 也恰好执行一次。它和「自己用布尔变量+锁实现」的区别只有一条:Once 把这个 易错的模式收进标准库,你不用每次重新踩一遍其中的竞态。

4.9 什么时候不用 channel 用锁(选择口径)

原书通篇「优先 channel」,但 §6 的表格给了反方向的线索。一条实用的口径:

  • 数据所有权在流动(生产者→消费者,谁拿着谁负责) → channel;
  • 数据定居在一处(一个配置表、一个缓存、一个计数器,大家都要来看) → 锁;
  • 读远多于写 → RWMutex;只是几个整数计数 → 原子操作(第 08 章展开)。

选错的代价不对称:该用锁的地方硬套 channel,会造出一个「地图管理员 goroutine」, 每次读数据都要发消息等回复,延迟凭空多一趟通信;该用 channel 的地方用锁, 则是把所有权弄糊,竞态风险回来。默认 channel,是真有定居数据时才用锁—— 这个次序本身也是原书的口径。

5. select:同时等好几路

select 和 switch 形似神不同:它在多个 channel 操作同时阻塞、谁先到执行谁; 配 time.After 就是超时,配 ctx.Done() 就是可取消——HTTP 客户端、流水线、 worker pool 的取消全指着它。(原书在退出模式和上下文两处处使用;第 06 章 context 是它的最大用家。18)

6. 竞态与 sync 包:主线例子修好它

竞态的严格定义,三条同时成立19:

  1. 两个以上 goroutine 并发访问同一变量;
  2. 至少一个在写;
  3. 没有同步定序。

counter++ 不是原子操作,是「读-改-写」三步;两个 goroutine 同时读到 100、 各自加一、先后写回 101——丢了一次加。期望 1000,打印 982 就是这么来的19。 -race 能抓(第 04 章),但要消除,得上同步。原书给的全家福20:

原语 干什么 主线例子的角色
sync.Mutex 互斥锁,同一时刻只放一个进临界区 修好计数器
sync.RWMutex 读锁共享、写锁独占 配置中心那种读多写少
sync.WaitGroup 等一组 goroutine 全部完工 替代 sleep 的瞎等
sync.Once 某初始化只执行一次 懒汉单例
sync.Cond 等「条件成立」的信号 有界队列
sync.Map 并发安全的 map 读多写少的共享表
sync.Pool 对象复用,减 GC 压力 高频临时对象

主线例子第 ② 步:Mutex 修计数器

var (
    counter = 0
    mu      sync.Mutex
)
func increment() {
    mu.Lock()
    counter++
    mu.Unlock()
}

锁住了「读-改-写」三步,三步变原子21。两条配套纪律:

  • defer mu.Unlock() 紧跟 Lock——早返回、panic 路径都不会忘解锁(第 04 章 defer 的兑现)22;
  • 粒度权衡:一把大锁简单但并行度低;每个字段一把锁并行高但易死锁。 没有标准答案,是设计题23。

主线例子第 ③ 步:WaitGroup 替代 sleep

time.Sleep(time.Second) 是「大概等够了」的瞎猜;WaitGroup 是点名报数:

var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        increment()
    }()
}
wg.Wait()              // 1000 个全报到了才往下走
fmt.Println(counter)   // 这次确定是 1000

Add(1) 记名、Done() 销号、Wait() 等清零24。程序不等完就退出的 「半成品写盘」事故,用这个根除。

RWMutex:读多写少的放大器

RLock() 读锁可以多个同时持有;Lock() 写锁独占。配置、缓存这类 「千次读一次写」的结构,吞吐显著提升25。边界:读者连绵不断时写者会饿死—— Go 的实现尽量缓解,但设计上要意识到25。

happens-before:锁为什么够用

光加锁不够,还要理解内存可见性。Go 规定了几条「happens-before」26:

动作 保证
Mutex 解锁 happens-before 之后的加锁(这次的写,下次的读看得见)
channel 发送 happens-before 对应的接收
close(channel) happens-before 收到零值

没有 happens-before 关系的写,读到什么是未定义的。锁和 channel 不只是 「别同时进」,还是「内存改动按序可见」的合同。

6.2 竞态的微观时间线:982 是怎么丢出来的

「丢更新」三个字太抽象,把第 1 节那个计数器在微观层面展开(数字为演示): 两个 goroutine 同时执行 counter++,这个操作其实是三步——

  时刻1:goroutine A 读 counter = 980
  时刻2:goroutine B 读 counter = 980   (A 还没写回)
  时刻3:A 加一,写回 981
  时刻4:B 加一,写回 981                  (覆盖了 A 的那次!)

两次加一,结果只涨了一。每一次「恰好同时」都是调度器随机赏的, 所以程序可能跑对一千次、在第一千零一次出 982——这也解释了为什么 「我本地没复现」不是证据,-race 的报告才是:它不赌时序,直接监视 每次内存访问有没有按 happens-before 排序。

6.3 锁粒度的实战口径(比原则更具体)

「粗锁简单、细锁快」之外,三条可以直接用的口径:

  1. 先粗后细:第一版一把大锁,profile 显示竞争成热点了再拆——过早细分 是优化之坑;
  2. 临界区里别调别人:锁内调用你不控制的代码(回调、网络),等于把锁的 寿命交给外人,死锁和性能惨案都从这来;
  3. 读路径先想 RWMutex,再想原子,最后才锁:三个方案按「实现成本」从低到高 是反的,按「性能」从低到高也是反的——取中。

7. 性能画像:工具链 + AI 的六步循环

7.1 先能看:开 pprof

import _ "net/http/pprof"   // 下划线导入:只为 init 注册处理器

go func() {
    http.ListenAndServe(":6060", nil)   // 浏览器/pprof 命令连这个端口
}()

pprof 能采样(周期性掐点统计,用样本推断全貌)CPU 时间、内存分配、goroutine 调度、阻塞事件、锁竞争27。 go tool pprof cpu.out 看火焰图;goroutine dump 看每个 goroutine 卡在哪行。

7.2 pprof 常见的六种病征28

goroutine 无上限疯涨(该用 worker pool)、锁粒度太粗(竞争热点)、channel 阻塞 (背压或饿死)、调度失衡、超时/取消优先级不对引发重试风暴、未同步访问。

7.3 worker pool:本章最重要的优化模式

解决什么问题: for _, task := range tasks { go handle(task) }——任务一多, goroutine 数量爆炸,上下文切换和 GC 压力一起吃光 CPU。 做法: 固定 N 个工人循环取任务;任务队列本身(有界 channel)就是天然的背压—— 生产者塞满就慢下来29:

jobs := make(chan Task, 100)
// N 个工人
for w := 0; w < 8; w++ {
    go func() {
        for j := range jobs { handle(j) }
    }()
}

7.4 AI 辅助的六步流程(原书的方法论,值得照抄)

  1. 采数据:go test -cpuprofile 或连线上 :6060;火焰图、goroutine dump、 trace、锁竞争文件各取所需30;
  2. 喂 AI:太大就剪最热的 20–50 行——「减过肥的输入 AI 消化得最好」30;
  3. 问具体问题(原书原话级别的示例):「解释这些 goroutine 为什么阻塞」 「这份 mutex trace 怎么减少竞争」「这段代码会有竞态吗,怎么修」31;
  4. 应用建议:比如把无缓冲 jobs 改成 100 缓冲、把每请求一个 goroutine 换成 有界池32;
  5. 重新画像验证——AI 的优化是假设,数字才是判决;不行就带着新数据再问33;
  6. 迭代到达标。

判断(我们的,不是书里的): 这六步的顺序(测→问→改→再测)比任何单个 技巧都值钱,因为它防住了 AI 辅助并发优化最大的风险:AI 提议的每一项改动 都可能引入新的竞态或死锁,而它自己不会跑 -race。第 4 步和第 5 步之间 永远隔着一次 go test -race ./...。 如果错,会错在: 若负载特征稳定、系统简单,直接照教科书模式(worker pool、 扇入扇出)可能胜过「AI 分析画像」的循环——六步流程的价值随系统复杂度上升, 小项目用它反而是仪式过度。

7.5 优雅关闭的完整时序(把第 06 章的流水线接进来)

第 06 章关的是 HTTP 服务器,这里把流水线的关闭接进同一时间线:

  SIGTERM 到
    ├─ HTTP:停止收新请求,等在途 handler 干完(带 10s 上限)
    ├─ 流水线:close(quit) 或 cancel(),每个环节在 select 里收到信号
    │         带着 defer close(out) 干净退出,一路 close 传回源头
    └─ 数据库:ctx 到期,在途查询中止,事务回滚,db.Close()

三个衔接点容易断:顺序(先停入口,再停加工,最后断仓库——反了会丢数据)、 每个 goroutine 都要有退出路径(第 04 章泄漏清单的对策)、关闭也要限时 (10 秒是上限不是目标,干不完的宁可回滚不要硬等)。原书把这三件事分散在 第 05、06、10 三章,合起来才是完整的关闭语义。

8. 作者的判断与证据

机制层面(可验证): G-M-P 模型、栈动态伸缩、异步抢占、channel 阻塞语义、 happens-before 规则表、race detector、各 sync 原语语义——全是运行时/规范事实582619。 建议层面(经验): 「先 channel 后锁」「defer 解锁」「粗粒度锁起步」是社区 最佳实践,本章未给测量依据;RWMutex 饿死风险的描述与官方文档一致2325。

9. 边界与局限

  • select 语句本身没单独立节——它散落在退出模式和上下文用例里,系统学要去别处。
  • context 包只点了名,取消传播树、value 的用法都推到第 06 章。
  • 原子操作(sync/atomic)只有一句警告(「需要小心使用」),没有示例—— 计数器这类简单场景其实原子操作比 Mutex 更省,读者需自行补充26。
  • AI 一节没有失败案例:喂错数据得出自信结论的风险,只在「Step 2 剪 20–50 行」 里隐性缓解。

10. 可带走的

  1. goroutine 起步 KB 级栈、G-M-P 调度、work stealing、可异步抢占——所以能开几万个。
  2. 循环里起 goroutine 传参 capture;闭包捕变量不捕值。
  3. 无缓冲 channel = 每次交接都握手(即时背压);有缓冲 = 队列(延迟背压)。
  4. 背压是设计选择:保正确性用无缓冲/小缓冲,保吞吐用大缓冲。
  5. 七个模式:生产者-消费者、流水线、扇出扇入、令牌通道、信号通知、地图管理员、优雅退出。
  6. 竞态三条件:同变量、有写、无同步。counter++ 三步走,必丢更新。
  7. Mutex + defer Unlock;粒度是设计题。RWMutex 治读多写少,注意写者饿死。
  8. WaitGroup 点名报数替代 sleep 瞎等。
  9. happens-before:解锁→加锁、发送→接收、close→收零值;没有它,读到什么未定义。
  10. worker pool 治 goroutine 爆炸,任务队列自带背压。
  11. 调优六步:测→喂→问→改→再测→迭代;AI 出假设,-race 和 pprof 出判决。
  12. 两个反直觉:缓冲不是越大越好;无缓冲在需要同步时反而快。

11. 原文地图

主题 原书章 原文位置
CSP 哲学与四原语 5. Concurrency in Go text/15-ch05-5-concurrency-in-go.txt:64(搜「concurrency paradigm influenced」)text/15-ch05-5-concurrency-in-go.txt:84(搜「The select statement enables」)
G-M-P 与异步抢占 同上 text/15-ch05-5-concurrency-in-go.txt:132(搜「Understanding Goroutines」)text/15-ch05-5-concurrency-in-go.txt:139(搜「G-M-P」)
循环变量捕获 同上 text/15-ch05-5-concurrency-in-go.txt:175(搜「Loop Variable Capture」)
无缓冲 channel 同上 text/15-ch05-5-concurrency-in-go.txt:218(搜「at the exact same moment」)
有缓冲 channel 同上 text/15-ch05-5-concurrency-in-go.txt:256(搜「behavior becomes synchronous」)
背压 同上 text/15-ch05-5-concurrency-in-go.txt:332(搜「Backpressure」)
四个误解 同上 text/15-ch05-5-concurrency-in-go.txt:344(搜「Common Pitfalls and Misconceptions」)
生产者-消费者 同上 text/15-ch05-5-concurrency-in-go.txt:382(搜「Producer–Consumer」)
流水线 同上 text/15-ch05-5-concurrency-in-go.txt:413(搜「Pipeline Communication」)
扇出/扇入 同上 text/15-ch05-5-concurrency-in-go.txt:455(搜「Fan-out and Fan-in」)
令牌通道 同上 text/15-ch05-5-concurrency-in-go.txt:496(搜「Token Channels」)
地图管理员 同上 text/15-ch05-5-concurrency-in-go.txt:542(搜「Avoiding Shared State」)
优雅退出 同上 text/15-ch05-5-concurrency-in-go.txt:572(搜「Graceful Shutdown」)
竞态三条件与计数器 同上 text/15-ch05-5-concurrency-in-go.txt:623(搜「Race Conditions in Go」)
sync 全家与 Mutex 同上 text/15-ch05-5-concurrency-in-go.txt:648(搜「Overview of the Sync」)text/15-ch05-5-concurrency-in-go.txt:664(搜「sync.Mutex」)
WaitGroup 同上 text/15-ch05-5-concurrency-in-go.txt:726(搜「not directly used for preventing data races」)
RWMutex 与饿死 同上 text/15-ch05-5-concurrency-in-go.txt:700(搜「Reader/Writer Locking」)
happens-before 表 同上 text/15-ch05-5-concurrency-in-go.txt:787(搜「Happens-before」)
pprof 开启 同上 text/15-ch05-5-concurrency-in-go.txt:831(搜「net/http/pprof」)
worker pool 收益 同上 text/15-ch05-5-concurrency-in-go.txt:855(搜「Worker Pools」)
AI 六步流程 同上 text/15-ch05-5-concurrency-in-go.txt:929(搜「Practical Guide」)


  1. 出处:「5. Concurrency in Go」第 53–81 段(text/15-ch05-5-concurrency-in-go.txt:64,搜「concurrency paradigm influenced」)。原文:「Go employs a concurrency paradigm influenced by Communicating Sequential Processes (CSP)」。 ↩

  2. 出处:同章第 82–90 段(text/15-ch05-5-concurrency-in-go.txt:84,搜「select statement enables」;sync、context 并列介绍)。 ↩

  3. 出处:同章第 623–646 段(text/15-ch05-5-concurrency-in-go.txt:639,搜「expected result might be 1000」;「behave correctly thousands of times」见 :609)。 ↩

  4. 出处:同章第 132–156 段(text/15-ch05-5-concurrency-in-go.txt:132,搜「Understanding Goroutines」;「few kilobytes」与「millions of goroutines」见 :136–138)。 ↩

  5. 出处:同章第 139–150 段(text/15-ch05-5-concurrency-in-go.txt:139,搜「G-M-P model」;work stealing 与异步抢占见 :144–146)。 ↩↩↩↩

  6. 出处:同章第 158–174 段(text/15-ch05-5-concurrency-in-go.txt:159,搜「go keyword」;匿名 goroutine 见 :166)。 ↩

  7. 出处:同章第 175–193 段(text/15-ch05-5-concurrency-in-go.txt:175,搜「Loop Variable Capture」)。「The variable's value changes during the loop」为原文明写(:183)。(新版本 Go 每轮新变量是对该语义的修正,属书外进展,标为通用知识。) ↩↩

  8. 出处:同章第 210–249 段(text/15-ch05-5-concurrency-in-go.txt:210,搜「Unbuffered Channels」;交棒比喻见 :248)。 ↩↩↩

  9. 出处:同章第 251–289 段(text/15-ch05-5-concurrency-in-go.txt:251,搜「Buffered Channels」;「Once the buffer is full, the behavior becomes synchronous」见 :256)。 ↩↩

  10. 出处:同章第 332–342 段(text/15-ch05-5-concurrency-in-go.txt:333,搜「Backpressure refers to」)。 ↩↩

  11. 出处:同章第 344–360 段(text/15-ch05-5-concurrency-in-go.txt:345,搜「Buffered channels always improve performance」)。 ↩

  12. 出处:同章第 382–411 段(text/15-ch05-5-concurrency-in-go.txt:382,搜「Producer–Consumer Coordination」)。 ↩

  13. 出处:同章第 413–453 段(text/15-ch05-5-concurrency-in-go.txt:413,搜「Pipeline Communication」;三 stage 代码见 :420)。 ↩

  14. 出处:同章第 455–494 段(text/15-ch05-5-concurrency-in-go.txt:455,搜「Fan-out and Fan-in」;三个 worker 的代码见 :466)。 ↩

  15. 出处:同章第 496–519 段(text/15-ch05-5-concurrency-in-go.txt:496,搜「Token channels」;「only 3 concurrent operations allowed」见 :504)。 ↩

  16. 出处:同章第 521–540 段(text/15-ch05-5-concurrency-in-go.txt:521,搜「Notifications and Signaling」)。 ↩

  17. 出处:同章第 542–570 段(text/15-ch05-5-concurrency-in-go.txt:542,搜「Avoiding Shared State」)。query/reply 代码见 :552。 ↩

  18. 出处:同章第 572–598 段(text/15-ch05-5-concurrency-in-go.txt:572,搜「Graceful Shutdown」;select+quit 代码见 :583)。 ↩↩

  19. 出处:同章第 623–646 段(text/15-ch05-5-concurrency-in-go.txt:624,搜「A race condition exists when」;三条件见 :625,读-改-写见 :640)。 ↩↩↩

  20. 出处:同章第 648–662 段(text/15-ch05-5-concurrency-in-go.txt:648,搜「Overview of the Sync Package」)。 ↩

  21. 出处:同章第 664–680 段(text/15-ch05-5-concurrency-in-go.txt:664,搜「Using sync.Mutex」)。 ↩

  22. 出处:同章第 682–692 段(text/15-ch05-5-concurrency-in-go.txt:690,搜「defer mu.Unlock」)。 ↩

  23. 出处:同章第 693–699 段(text/15-ch05-5-concurrency-in-go.txt:693,搜「Mutex Granularity」)。 ↩↩

  24. 出处:同章第 725–740 段(text/15-ch05-5-concurrency-in-go.txt:726,搜「not directly used for preventing data races」)。 ↩

  25. 出处:同章第 700–723 段(text/15-ch05-5-concurrency-in-go.txt:700,搜「Reader/Writer Locking」;starvation 见 :721)。 ↩↩↩

  26. 出处:同章第 787–800 段(text/15-ch05-5-concurrency-in-go.txt:787,搜「Happens-before」;四条规则见 :793)。 ↩↩↩

  27. 出处:同章第 824–843 段(text/15-ch05-5-concurrency-in-go.txt:824,搜「pprof—allow detailed sampling」)。 ↩

  28. 出处:同章第 844–853 段(text/15-ch05-5-concurrency-in-go.txt:844,搜「systemic patterns」)。 ↩

  29. 出处:同章第 855–884 段(text/15-ch05-5-concurrency-in-go.txt:855,搜「Worker Pools as a Concurrency」;四条收益见 :863)。 ↩

  30. 出处:同章第 929–956 段(text/15-ch05-5-concurrency-in-go.txt:929,搜「Practical Guide」;「top 20–50 lines」见 :955)。 ↩↩

  31. 出处:同章第 958–968 段(text/15-ch05-5-concurrency-in-go.txt:959,搜「targeted prompts」)。六个问题示例均为原书原话(英文)。 ↩

  32. 出处:同章第 970–992 段(text/15-ch05-5-concurrency-in-go.txt:970,搜「Apply AI-Suggested Refactorings」)。 ↩

  33. 出处:同章第 994–1006 段(text/15-ch05-5-concurrency-in-go.txt:994,搜「Re-Profile and Validate」)。「AI optimization is hypothesis-driven. Verification is still grounded in measurement.」为原文明写。 ↩