并发处理流水线 — 从三环节管道到能扛流量的扇入扇出¶
这一章讲什么: 把 goroutine 和 channel 组装成真正处理数据的流水线—— 环节怎么切、流量怎么控、错误怎么流、结果怎么合、性能怎么画像。 它在全书链条里的位置: 第 05 章给了你并发原语,这一章给你原语的装配图纸。 前面章节出现的每一个「后台处理」(读日志、搬数据、跑批)在这一章都长成 可维护的形状;第 10 章的性能调优从这里拿工具。 主线例子(贯穿本章): 一条数数流水线
generator → filterEven → square, 逐步升级:先组合化,再扇出成 4 个工人,再用 pprof 抓出「150 个 goroutine 卡在 chan send」的病,最后用 AI 建议的缓冲尺寸治好。
1. 顶层全景:流水线的三段式¶
source(源)──► stage(变换)──► stage(变换)──► sink(汇)
读文件/队列 清洗/解析/计算 过滤/聚合 存库/发指标
│ │ │ │
└──────── 每个环节一个 goroutine,环节之间一条 channel ───────┘
原书给的最小流水线(这就是本章的「Hello World」)1:
func generator(out chan<- int) {
for i := 1; i <= 5; i++ { out <- i }
close(out)
}
func square(in <-chan int, out chan<- int) {
for n := range in { out <- n * n }
close(out)
}
func printer(in <-chan int) {
for v := range in { fmt.Println("Result:", v) }
}
三个原则,原书明说它们「scale to production systems」2:
- 每个环节隔离——generator、square、printer 各自独立,可单独测、单独换;
- channel 定义流向——同步由数据流自然产生,不外加锁;
- 终止要显式——谁发完谁 close,下游 range 到 close 自动收工。
2. 设计决策一:背压与流控¶
各环节速度不一时,上游快下游慢,内存和 goroutine 会无界膨胀——背压就是 让「快」等「慢」的机制(第 05 章的 §3.3 在这里兑现为设计准则)3:
- 环节间紧耦合(要求严格交接) → 无缓冲 channel;
- 允许突发(短暂不均衡) → 小缓冲;
- 大缓冲慎用——它把性能问题藏起来,直到内存尖刺那一刻爆发。
3. 设计决策二:错误怎么流过环节¶
普通值走 data channel,错误走哪?原书给的三条路4:
- Item 包装结构(最常用)——错误和数据坐同一条传送带,天然避免两条 channel 之间的竞态:
- 专用 error channel——错误要集中聚合或统一记录时;
- 早退 + 收尾纪律——某环节遇到不可恢复错误提前退出时,必须 close 自己的
输出 channel,否则下游永远 range 等下去。
defer close(out)是标配4。
4. 设计决策三:取消与可组合¶
取消: 环节的主循环用 select 同时听 ctx.Done() 和数据,取消信号一到
立刻带着 defer close 干净退出——流水线整体可停,是接 HTTP、接定时任务的
前提5。
可组合(本章最美的写法): 阶段函数统一形状——收一个只读 channel, 返回一个只读 channel,内部自带 goroutine 和 close**:
func multiply(factor int) Stage {
return func(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for v := range in { out <- v * factor }
}()
return out
}
}
于是流水线变成函数嵌套,可读性极高6:
主线例子第 ① 步: generator(10) 吐 1…10,filterEven 留偶数,
square 平方——消费者依次看到 4、16、36、64、100(过滤后 2,4,6,8,10 各自平方)。
这个嵌套就是原书的完整示例,三个环节各一件事、可替换、可重排7。
5. 扇出与扇入:并行的两条腿¶
扇出(fan-out): 多个 worker 共享一个输入 channel,任务自动负载均衡—— channel 的接收语义天然谁空谁拿。worker 数是主要的并行度旋钮8:
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ { go worker(w, jobs, results) }
for i := 1; i <= 10; i++ { jobs <- i }
close(jobs) // 发完必关:工人靠它收工
for i := 1; i <= 10; i++ { fmt.Println(<-results) }
扇入(fan-in): 多条输出流合成一条。关键纪律只有一个—— 等所有上游干完再 close 汇总 channel,用 WaitGroup 把关9:
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
wg.Add(len(channels))
for _, ch := range channels {
go func(c <-chan int) {
defer wg.Done()
for v := range c { out <- v }
}(ch)
}
go func() { wg.Wait(); close(out) }()
return out
}
主线例子第 ② 步: 原书的组合例——generator(1..8) 扇出给 4 个
squareWorker,fanIn 四条流合一条,消费者 range 到结束10。
注意此时结果的顺序不再保证(四个工人手速不同)——需要保序的场景,
得给结果挂上序号再重排,这是本章没展开但读者必撞的边界。
设计清单(原书四条):close 必须显式确定;worker 数匹配 CPU 核数或 I/O 并行度;缓冲减震但别过度;ctx 集成以支持优雅终止11。
6. worker pool 与四种调度¶
解决什么问题: for _, item := range items { go process(item) }——
一万个 item 起一万个 goroutine,调度开销和 GC 反而把吞吐拖垮。
worker pool: 固定 N 个工人循环取任务,资源用量可预测12。
N 怎么定?原书的经验法则:CPU 密集型约等于核数(线性扩展到核数为止); I/O 密集型可以更多——许多工人在等网络,不占 CPU13。
channel 自带 FIFO 调度(先进先出),同构任务够用;任务轻重不一时, 原书列了三种进阶14:
| 调度策略 | 做法 | 何时用 |
|---|---|---|
| 工作窃取 | 每工人一个私有队列,闲的从忙的偷 | 任务时长差异大(标准库不自带,要自己搭) |
| 优先级 | 集中式 dispatcher 按优先级排序后再发 | 实时管道,紧急任务插队 |
| 限流 | time.Ticker 掐着表发任务 |
调外部 API,防冲垮对方 |
动态池: 盯队列深度、工人闲时、处理延迟、CPU,忙了加人(有上限)、闲了裁——
云编排平台的伸缩逻辑,缩小版15。可取消的工人: 主循环 select ctx.Done(),
请求超时/用户取消/系统关停三种情况都接得住16。
6.1 动态池的四个信号(原书清单的展开)¶
固定池简单,但负载波动大时不是浪费(闲工人)就是排队(忙时工人不够)。 动态池靠四个信号决定是否加人、减人:
| 信号 | 怎么读 | 动作 |
|---|---|---|
| 队列深度 | 任务 channel 的缓冲积压量 | 积压持续上涨 → 加人 |
| 工人闲时 | 工人多久没接到活 | 长期空闲 → 减人 |
| 处理延迟 | 任务从入队到出结果的时间 | 延迟超目标 → 加人 |
| CPU 用量 | 机器还有多少余量 | 快到顶了,加人也没用,该限流 |
两个护栏比信号本身更要紧:上限(加到多少就停——通常核数的倍数)和 冷却(加减人别太频繁,否则池子自己在抖动)。原书只给了信号清单, 控制循环的具体实现留给读者——落地时记住:这是「反馈控制」,参数宁钝勿灵。
6.2 批处理:什么时候「攒一批」是对的¶
AI 优化例四的批处理处方,值得说清边界。「攒 100 条再算」有效的前提是: 每条的固定开销(函数调用、状态切换、内存分配)比数据本身的处理成本显眼。 对哈希这种重计算,摊薄固定开销收益大;对一次数据库写入,攒批反而增加延迟 (前面的记录在等最后一条)。判据一句话:下游按批量计价就攒批,按条计价就不攒。
6.3 trace 画像与运行时旋钮¶
第 5 章 pprof 清单之外,trace 画像(记录调度器全部事件)回答的是
「为什么这一刻全世界都停了」:GC 的 stop-the-world、goroutine 被挂起
(Goroutine Park)、被唤醒(Ready)、跨 P 偷活(Proc Steal)都在事件流里。
配套的旋钮:CPU 密场景调 GOMAXPROCS;goroutine 数量爆炸时,
与其加人不如减任务(回到池化)。trace 文件大,先看火焰图和 goroutine 画像,
它们答不了再看 trace——这是原书没写、但 pprof 用户的共同经验(通用知识,不在原书)。
7. channel 沟通策略五条(原书汇总,本章的「语法书」)¶
- 方向类型(
chan<-/<-chan):函数签名里锁死方向,编译器替你守住 「只读的别写、只写的别读」——长管道多人协作时的架构保险17; - 缓冲决策:无缓冲=严格同步点;有缓冲=邮箱。太小卡、太大藏问题, 靠 benchmark 找平衡17;
- 关闭约定:只有发送方 close;接收方用
v, ok := <-ch侦测关闭; 双向系统另开 done/quit channel 传取消,别拿主数据流兼任18; - select 多路复用:同时听任务、取消、心跳,谁到处理谁;
default分支 可做非阻塞发送/过载丢任务。原书告诫:分支别太多,每支的意图要写注释19; - 扇入扇出的协调:多生产者共享输出、单生产者分发多工人,全靠 channel 定界——正确的 close 时序是生死线19。
8. pprof:给流水线拍五种 X 光¶
第 05 章开了 pprof 的门,这一章教读片。五种画像各看一种病20:
| 画像 | 看什么 | 典型病征 |
|---|---|---|
| CPU | 哪些函数吃 CPU | 热点变换、JSON 编码、反复建工人 |
| Goroutine | 此刻每个 goroutine 卡在哪 | 几千个卡在 chan send = 背压爆掉 |
| Block | 谁在同步原语上等待 | channel 等待、锁竞争、无缓冲的停顿 |
| Heap | 分配和 GC | 循环里藏分配、大临时缓冲 |
| Trace | 运行时全景事件 | 流水线卡顿、调度器抽风、STW 暂停 |
采集照旧:import _ "net/http/pprof" + :6060,然后
go tool pprof http://localhost:6060/debug/pprof/goroutine21。
三个诊断故事(原书实拍):
- channel 瓶颈: goroutine 画像里一大片
goroutine 200 [chan send]—— 生产者在等消费者。处方:加缓冲、加工人、缩小批量、或主动丢任务(load-shedding)22; - 工人不均: CPU 画像一个工人爆红、其他摸鱼——哈希(把任意数据算成一个整数的函数,相同输入必得相同输出)分键倾斜、长任务霸坑。 处方:随机分派、批量、按工人盯队列深度23;
- 内存尖刺: heap 画像里
encoding/json独占 120MB——换sync.Pool复用缓冲或更快的编解码器24。
火焰图读法(原文一句话):从下(根)往上(叶)堆函数,越宽 = 越久; 并发流水线用它找热环节、贵系统调用、工人里啰嗦的日志25。
处方集(对着画像开药):减阻塞(缓冲/加人/重平衡)、减分配(复用/预分配/少拷贝)、 提并行(长环节拆子管道、小任务合批)、减 goroutine(调度器抖动时, 一千个轻任务不如几十个协调好的工人)、调运行时(GOMAXPROCS、GC 步调、流式控内存)26。
9. AI 辅助优化:四个实例(辅线)¶
原书这一节的好处在每个例子都从真实病征出发。四个:27
例一:缓冲尺寸建议。 负载测试 CPU 低吞吐差,goroutine 画像 150 个
[chan send] 堵在 producer→jobs。贴给 AI,它答:「工人处理慢于生产,
无缓冲 channel 把生产者卡住了——按突发量或工人吞吐加缓冲」。改
make(chan Job, 200) 后吞吐立升。(注意:200 这个数是起点不是真理,
最终值要靠再画像迭代。)
例二:朴素扇出改 worker pool。 go func(v){ out <- process(v) }(item)
被认出「每件一起 goroutine」的反模式;AI 建议 runtime.NumCPU()*2 个稳定工人
——栈内存复用、调度开销降、延迟变平。(*2 同样是启发值,不是定律。)
例三:隐蔽死锁的消除。 merge 函数看着无害,高负载下偶发挂死:
goroutine dump 一片 blocked on send to out。AI 的根因:「out 无缓冲,
扇入工人往外发时卡住,而读的消费者又在忙别的」。处方:out := make(chan int, 1000),
或者重新设计让消费者不至于饿死工人。
例四:CPU 密集环节改批处理。 transform 环节每条记录一次哈希,
火焰图显示它独吞 CPU。AI 建议攒 100 条一批再算——省 20–30% 分配、
CPU 利用率上升、调用开销被摊薄。(这四例的共同结构:画像出症状 → AI 出假设
→ 改代码 → 再画像确认。)
判断(我们的,不是书里的): 例一、例三的处方都是「加缓冲」,但两例的病根 不同:例一是吞吐问题(缓冲有效),例三是死锁(缓冲只是缓解了消费者饿死工人的 时序)。AI 的建议在这两例里恰好都对,但它不会替你做这个区分——「加缓冲」 是它最顺口的答案,而缓冲恰恰是藏问题的高手(§2)。所以处方落地前, 先问一句:这是要提吞吐,还是要改时序? 如果错,会错在: 若流水线瓶颈根本不在 channel 而在下游(数据库慢), 加缓冲只是把排队从「goroutine 卡着」变成「内存堆着」,AI 的画像解读 训练自「channel 是病根」的常见案例,会系统性高估缓冲的价值。
9.1 四个 AI 例之外的通用教训¶
把第 9 节的四个例子抽掉细节,剩下的方法论是四条:
- 画像里的状态字符串就是诊断书。
chan send/chan receive/select/syscall这几种状态,各自对应一类病(背压/饿死/死锁/I/O 慢),先认状态 再谈优化; - 改一处,看一处。 每次只改一个变量(缓冲、工人数、批大小), 再画像对比——同时改三处,好了不知为什么好,坏了不知坏在哪;
- 吞吐和延迟分开验收。 缓冲常常涨吞吐也涨尾延迟(队伍变长); SLO 里两个都有目标时,优化要对着两个数字说话;
- AI 的建议里永远先问「它假设了什么」。 例一假设瓶颈在 channel; 若真瓶颈在数据库,加缓冲只是把排队挪进内存(第 1 轮加厚时已展开过这条)。
9.2 与真实系统的对应(原书开篇的清单)¶
原书开篇把流水线和三类生产系统画了等号,值得记下这张对应表——面试和架构评审 都用得上:
| 生产系统 | 对应流水线的哪部分 |
|---|---|
| 流处理器(Kafka/Flink 那种) | 多环节 + 扇出扇入 + 有界缓冲 |
| ETL(抽数-变换-装载) | source → 多个 transform → sink |
| 事件驱动架构 | channel 即事件总线,goroutine 即消费者 |
| 队列处理微服务 | worker pool + 背压 |
Go 写这些系统的竞争力不在「能写」,而在同样的形状,代码量是他语言的零头—— 本章所有模式加起来不到 200 行示例,这是 goroutine+channel 模型的复利息。
10. 作者的判断与证据¶
硬事实: channel 语义、close 规则、select、WaitGroup 关闸、pprof 五画像的 存在与读法——工具链级事实1820。经验值: worker 数=核数×2、缓冲 200/1000、 批 100——全是启发值,原书也未声称普适,例二里 AI 自己说的「based on CPU cores」 只是起步建议27。判断句:「Profiling builds visibility. AI builds understanding」 ——总结漂亮,但按我们的立场,AI 给的至多是有根据的猜测,understanding 还是人的28。
11. 边界与局限¶
- 保序与精确一次两个硬核问题没展开:扇入后乱序怎么办?环节崩溃重投怎么办? 这俩是消息系统教科书章节,本章止于「形状正确」。
- 背压的量化方法(缓冲多大算够)只说「benchmark」,没给操作步骤。
- 动态池没有完整实现,只有监控指标清单——动手要自补控制循环。
- AI 四例全是成功故事,没有「AI 误诊」案例;按第 05 章的六步流程, 误诊本来就该被「再画像」拦住,但读者要有这个警觉。
12. 可带走的¶
- 流水线三原则:环节隔离、channel 定流、终止显式(谁发完谁 close)。
- 背压是设计选择:无缓冲=紧耦合,小缓冲=吸收突发,大缓冲=藏问题。
- 错误用 Item{Value, Err} 包装流,或专用 error channel;早退必 close 输出。
- 阶段函数统一「收 <-chan 返 <-chan」形状,流水线=函数嵌套。
- 扇出共享输入(负载自动均衡),扇入 WaitGroup 把关后再 close。
- 扇入后顺序不保证——保序要挂序号。
- worker pool:CPU 密≈核数,I/O 密可多些;FIFO 不够再有优先级/窃取/限流。
- 关闭纪律:只有发送方 close;取消走独立 done channel。
- pprof 五画像:Goroutine 看背压、Block 看等待、Heap 看分配、CPU 看热点、Trace 看卡顿。
- 火焰图:从下往上,越宽越久。
- AI 四板斧:建议缓冲、池化重构、死锁消除、批处理——但先分清病根是吞吐还是时序。
- 所有数字(200、1000、×2、100)都是起点,终点永远在再画像里。
13. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 最小流水线三原则 | 8. Concurrent Processing Pipelines | text/18-ch08-8-concurrent-processing-pipelines.txt:74(搜「minimal pipeline」);三原则见 :100 |
| 背压与流控 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:106(搜「Backpressure and Flow Control」) |
| 错误策略(Item 包装) | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:124(搜「Error Handling Strategies」) |
| ctx 取消环节 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:147(搜「Pipeline Cancellation」) |
| 可组合阶段 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:172(搜「Composing Pipelines」);完整组合例见 :195 |
| 扇出 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:254(搜「Understanding Fan-out」) |
| 扇入 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:303(搜「Understanding Fan-in」) |
| 扇出+扇入组合 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:360(搜「Combining Fan-out」) |
| worker pool 核心 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:415(搜「Worker Pools」);N 与负载关系见 :436 |
| 四种调度 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:471(搜「Task Scheduling」);限流见 :515 |
| 动态池与可取消池 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:542(搜「Dynamic Worker Pools」)text/18-ch08-8-concurrent-processing-pipelines.txt:556(搜「Cancellable Pools」) |
| channel 五策略 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:579(搜「Channel-Based Communication」) |
| pprof 五画像 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:678(搜「What pprof Measures」) |
| 激活与采集 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:733(搜「Activating pprof」) |
| 三个诊断故事 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:766(搜「Channel Bottlenecks」)、:779(搜「Uneven Worker」)、:798(搜「Memory Spikes」) |
| 火焰图读法 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:819(搜「Interpreting Flamegraphs」) |
| AI 四例 | 同上 | text/18-ch08-8-concurrent-processing-pipelines.txt:905(搜「AI-Assisted Optimization Examples」) |
-
出处:「8. Concurrent Processing Pipelines」第 74–99 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:74,搜「minimal pipeline」)。 ↩ -
出处:同章第 100–104 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:100,搜「three key principles」)。 ↩ -
出处:同章第 106–122 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:106,搜「Backpressure and Flow Control」)。 ↩ -
出处:同章第 124–146 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:124,搜「Error Handling Strategies across Stages」;Item 结构见 :131)。 ↩↩ -
出处:同章第 147–170 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:147,搜「Pipeline Cancellation with Context」;squareWithContext 见 :153)。 ↩ -
出处:同章第 172–194 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:172,搜「Composing Pipelines for Reusability」;multiply 例见 :178)。 ↩ -
出处:同章第 195–241 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:195,搜「Fully Composed Example」)。输出 4,16,36,64,100 由我们按代码推演(为演示说明)。 ↩ -
出处:同章第 254–301 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:254,搜「Understanding Fan-out」;三工人例见 :274)。 ↩ -
出处:同章第 303–358 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:303,搜「Understanding Fan-in」;fanIn 实现见 :335)。 ↩ -
出处:同章第 360–394 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:360,搜「Combining Fan-out and Fan-in」)。 ↩ -
出处:同章第 396–413 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:396,搜「Design Considerations and Best Practices」)。 ↩ -
出处:同章第 415–469 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:415,搜「Worker Pools and Task Scheduling」;五工人例见 :447)。 ↩ -
出处:同章第 436–439 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:436,搜「CPU-bound workloads」)。 ↩ -
出处:同章第 471–524 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:471,搜「Task Scheduling in Worker Pools」;优先级 dispatcher 见 :497,限流 ticker 见 :515)。 ↩ -
出处:同章第 542–554 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:542,搜「Dynamic Worker Pools」)。 ↩ -
出处:同章第 556–577 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:556,搜「Cancellable Pools」)。 ↩ -
出处:同章第 579–615 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:579,搜「Channel-Based Communication Strategies」;方向类型见 :590)。 ↩↩ -
出处:同章第 616–628 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:616,搜「channel closing conventions」;「only the sender should close」见 :621)。 ↩↩ -
出处:同章第 629–659 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:629,搜「communication design pattern」)。 ↩↩ -
出处:同章第 678–731 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:678,搜「Understanding What pprof Measures」)。 ↩↩ -
出处:同章第 733–758 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:733,搜「Activating pprof」)。 ↩ -
出处:同章第 766–777 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:766,搜「Detecting Channel Bottlenecks」;goroutine 200 [chan send]见 :769)。 ↩ -
出处:同章第 779–789 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:779,搜「Finding Uneven Worker Utilization」)。 ↩ -
出处:同章第 798–803 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:798,搜「Identifying Memory Spikes」;120MB 例见 :800)。 ↩ -
出处:同章第 819–828 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:819,搜「Interpreting Flamegraphs」)。 ↩ -
出处:同章第 829–851 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:829,搜「Optimizing Pipelines Using pprof」)。 ↩ -
出处:同章第 905–1061 段(
text/18-ch08-8-concurrent-processing-pipelines.txt:905,搜「AI-Assisted Optimization Examples」;四例分别见 :908、:937、:987、:1022)。 ↩↩ -
出处:同章第 994–1013 段(
text/15-ch05-5-concurrency-in-go.txt:994,搜「Re-Profile」段落后;「Profiling builds visibility」总结见第 05 章同文件text/15-ch05-5-concurrency-in-go.txt:1007)。 ↩