题记与说明
- 日期:2026-11-03
- 前缀:knowledge-3me31c2q-4
- 类型:主题研究(检索 8 个来源精读 + 本地 Go 1.24.5 实测复现 8 项断言)
- 来源(本主题的一手来源,全部在本会话实际读取):
- Sameer Ajmani, *Go Concurrency Patterns: Pipelines and cancellation*, The Go Blog, 2014-03-13 — https://go.dev/blog/pipelines (全文精读)
- *Go Concurrency Patterns: Context*, The Go Blog, 2014-07-29 — https://go.dev/blog/context (全文精读)
- Andrew Gerrand, *Error handling and Go*, The Go Blog, 2011 — https://go.dev/blog/error-handling-and-go (全文精读)
- Leonidas Vrachnis, *Graceful Shutdown in Go*, RudderStack Engineering Blog, 2021-11-09 — https://www.rudderstack.com/blog/implementing-graceful-shutdown-in-go/ (WebFetch 摘读,含原文引述)
- Andrew Gerrand, *Share Memory By Communicating*, The Go Blog, 2010-07-13 — https://go.dev/blog/codelab-share (WebFetch 摘读)
- Rob Pike, *Concurrency is not Parallelism*(Heroku Waza 演讲 slide), 2012 — https://go.dev/talks/2012/waza.slide (WebFetch 摘读,逐句引述)
- *Go 1.20 Release Notes*(errors.Join / 多重 %w / context.WithCancelCause)— https://go.dev/doc/go1.20 (WebFetch 摘读)
- *Go 1.22 Release Notes*(for 循环变量逐迭代新建)— https://go.dev/doc/go1.22 ;及 context 包文档 https://pkg.go.dev/context (WithValue 使用警告)
- 本地实测环境:Windows 10.0.26200 x64,
go version go1.24.5 windows/amd64(本会话go version实际输出)。实验代码位于工作区go-concurrency-demo/(6 个 demo + 1 个 HTTP 优雅退出 demo,仅标准库),经go vet ./...与go run/go build实际执行,完整输出见「四、本会话执行记录」。
一、七维提取
1. 核心问题
Go 把"并发"当作结构化程序的手段而非性能补丁。整个模型要回答的是:在一门面向大规模网络服务的语言里,如何让成千上万个并发单元安全地组合、通信、出错、退出。拆成四个子问题,恰好对应标题四个关键词:
- goroutine:并发单元应该多便宜、如何调度,才能让"每个请求一个 goroutine、每个后端调用再开 goroutine"成为默认写法?
- channel:并发单元之间如何通信,才能不回到"共享内存 + 锁"的老路?(CSP 路线)
- context:当一个请求被取消/超时/出错,如何把"停止"信号传播给数量未知的一组 goroutine?
- 错误处理 + 优雅退出:错误如何跨 goroutine 边界显式搬运与聚合?进程收到 SIGTERM 后如何做到"不丢数据、不断 in-flight"地退场?
Pipelines 一文开篇点出难点所在——并发的快乐路径容易,失败路径才是真问题:
Go's concurrency primitives make it easy to construct streaming data pipelines that make efficient use of I/O and multiple CPUs. This article presents examples of such pipelines, highlights subtleties that arise when operations fail, and introduces techniques for dealing with failures cleanly.
2. 主要主张
(a)并发 ≠ 并行;goroutine 是结构工具(Rob Pike, waza.slide)
"Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once."
"Concurrency is about structure, parallelism is about execution."
"(They're a bit like threads, but they're much cheaper.) Goroutines are multiplexed onto OS threads as required. When a goroutine blocks, that thread blocks but no other goroutine blocks."
"It's routine to create thousands of goroutines in one program. (Once debugged a program after it had created 1.3 million.) Stacks start small, but grow and shrink as required. Goroutines aren't free, but they're very cheap."
结论句:"Concurrency is powerful. Concurrency is not parallelism. Concurrency enables parallelism."
(b)通信优先于共享内存(CSP 哲学,Share Memory By Communicating)
Effective Go 格言(该文引用):
"Do not communicate by sharing memory; instead, share memory by communicating."
该文的核心论证:传统线程模型里数据被锁保护、线程争锁,导致冗长的簿记代码;Go 做法是通过 channel 传递数据的引用,任一时刻只有一个 goroutine 持有该数据——所有权转移而非共享。对比示例中原本一整页的 Poller 锁逻辑,channel 版本是:
func Poller(in, out chan *Resource) {
for r := range in {
// poll the URL
out <- r
}
}"The delicate logic from the previous example is conspicuously absent, and our Resource data structure no longer contains bookkeeping data. In fact, all that's left are the important parts."
(c)管道模式与生命周期不变式(Pipelines and cancellation)
管道定义:"a pipeline is a series of stages connected by channels, where each stage is a group of goroutines running the same function."。fan-out/fan-in 定义:多个函数读同一 channel 叫 fan-out;一个函数多路读入、汇成单 channel 叫 fan-in。
三条硬性主张(本会话全部实测复现,见第四节):
"Sends on a closed channel panic, so it's important to ensure all sends are done before calling close."
"This is a resource leak: goroutines consume memory and runtime resources, and heap references in goroutine stacks keep data from being garbage collected. Goroutines are not garbage collected; they must exit on their own."
"In Go, we can do this by closing a channel, because a receive operation on a closed channel can always proceed immediately, yielding the element type's zero value."(close 即广播——done channel 模式的根基)
管道建造守则(原文):
- stages close their outbound channels when all the send operations are done.
- stages keep receiving values from inbound channels until those channels are closed or the senders are unblocked.
(d)context 是取消传播的标准答案(Go Concurrency Patterns: Context)
"When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using."
关键设计决策——Context 故意没有 Cancel 方法:
"A Context does *not* have aCancelmethod for the same reason theDonechannel is receive-only: the function receiving a cancellation signal is usually not the one that sends the signal."
Context 树:Background 是根、永不取消;WithCancel/WithTimeout 派生可提前取消的子节点;"when a Context is canceled, all Contexts derived from it are also canceled.";"A Context is safe for simultaneous use by multiple goroutines."(一个 ctx 广播给任意多个 goroutine)。
Google 的工程纪律(原文):
"At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests."context 包文档对 WithValue 的官方警告(pkg.go.dev/context Overview):
"Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions."
"Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it."
(e)错误是值,显式检查、显式传播(Error handling and Go)
"In Go, error handling is important. The language's design and conventions encourage you to explicitly check for errors where they occur (as distinct from the convention in other languages of throwing exceptions and sometimes catching them). In some cases this makes Go code verbose…"
错误必须自带上下文(错误实现的职责):
"It is the error implementation's responsibility to summarize the context. The error returned by os.Open formats as 'open /etc/passwd: permission denied,' not just 'permission denied.'"两类消费方式:fmt.Errorf 加上下文给"路过的调用方";自定义错误类型(如 json.SyntaxError 带 Offset、net.Error 带 Timeout()/Temporary())给"想据此决策的调用方"做类型断言。并发场景下的特有约束(pipelines 文中的 result struct / 缓冲为 1 的 errc):goroutine 里的错误必须放进 channel 搬出来,异常不会替你跨越 goroutine 边界。
(f)错误聚合与取消原因的现代答案(Go 1.20)
"The new function errors.Join returns an error wrapping a list of errors."
"Go 1.20 expands support for error wrapping to permit an error to wrap multiple other errors."(Unwrap() []error)
"The errors.Is and errors.As functions have been updated to inspect multiply wrapped errors."
"The fmt.Errorf function now supports multiple occurrences of the %w format verb…"
"The new WithCancelCause function provides a way to cancel a context with a given error. That error can be retrieved by calling the new Cause function."
(g)经典坑被语言版本修掉(Go 1.22)
"Previously, the variables declared by a 'for' loop were created once and updated by each iteration. In Go 1.22, each iteration of the loop creates new variables, to avoid accidental sharing bugs."
即:Go 1.22+(go.mod 声明 ≥1.22)中 for i := …; … { go func(){ use(i) }() } 捕获的 i 不再共享——本会话实测输出 [0 1 2](第四节 demo4)。
(h)优雅退出 = 信号 → context 广播 → 停生产 → drain → 限时兜底(RudderStack)
反模式(原文):
Calling os.Exit(1) while goroutines run — "essentially equal to SIGKILL, no chance for closing open connections."信号不是广播(原文):
"Go channels do not work in a broadcast way, only one go routine will receive a single os.Signal."
所以要用 signal.NotifyContext(Go 1.16+)把信号变成 context 取消,让所有 goroutine 都能 <-ctx.Done()。等待机制三档:裸 channel(单 goroutine 才实用)→ sync.WaitGroup(多个)→ errgroup(再加错误传播 + 出错自动取消派生 ctx)。http.Server.Shutdown 的语义(原文):
"first clos[es] all open listeners, then clos[es] all idle connections, and then wait[s] indefinitely for connections to return to idle."
组件顺序约束:先取消生产者,等它们退出,再 close channel 让消费者 drain——顺序反了就会向已关闭 channel 发送(panic)或丢数据。退出兜底:Shutdown 外面套 context.WithTimeout,超时强杀。
3. 方法
- 官方博客的论证法:模式归纳 + 反例驱动。Pipelines 一文先给出会 goroutine 泄漏的代码,再经历"缓冲通道补丁(作者自称 'this is bad code',因为缓冲大小依赖对上下游计数)→ 手工 done channel(每个接收方要数清上游发送者个数,'tedious and error-prone')→ close(done) 广播"三步演进,最终收敛为两条守则。这是"从失败的代码逼近正确不变式"的写法。
- 类比论证(Rob Pike):地鼠运书烧毁——更多地鼠(goroutine)不够,还需要更多小车(channel/缓冲);瓶颈在堆和焚化炉(源头与汇);"This design is not automatically parallel! … However, it's automatically parallelizable!"——并发设计先行,并行性是映射上去的属性(gopher→CPU,cart→marshaling/networking,incinerator→consumer)。
- 工程清单法(RudderStack):按"信号捕获 → 等待机制选型 → context 传播 → HTTP Server → HTTP Client → channel drain → 组件生命周期模式(Run(ctx) 或 Setup/Shutdown 对)"逐层给可抄的代码骨架。
- 语言演进对照:把 2010-2014 的经典文章放到 Go 1.20/1.22 的演进下校准——哪些模式已被标准库吸收(errgroup、signal.NotifyContext、errors.Join、循环变量修复),哪些博文代码已过时(
ioutil.ReadFile、手工 done channel 传播)。 - 本地实测复现(本会话新增):用 go1.24.5 跑 6 个最小 demo + 1 个 HTTP 优雅退出对照实验,把文献断言变成一手输出(详见第四节)。
4. 数据与证据
(a)文本证据:上面 2.(a)-(h) 的引述均逐字来自本会话抓取的 8 个来源;引述索引见第三节。
(b)本地实测(本会话执行,go vet ./... && go run .,go1.24.5 windows/amd64,完整原始输出见第四节):
| 实验 | 验证断言(出处) | 实测结果 |
|---|---|---|
| demo1 | 已关闭 channel 接收立即返回零值(pipelines:"receive operation on a closed channel can always proceed immediately") | 立即返回,耗时 0s,值类型 struct {} |
| demo2 | 向已关闭 channel 发送 panic(pipelines:"Sends on a closed channel panic") | panic:send on closed channel |
| demo3 | goroutine 泄漏(pipelines:"Goroutines are not garbage collected") | 无接收者的阻塞发送 goroutine 使 runtime.NumGoroutine() 1→2,50ms 后仍存活 |
| demo4 | Go 1.22+ 循环变量逐迭代新建(go1.22 release notes) | goroutine 捕获 i 输出 [0 1 2](共享旧语义应为 [3 3 3]) |
| demo5a | errors.Join + errors.Is 穿透多重包装(go1.20 release notes) | errors.Is(joined, errA)=true, errors.Is(joined, errB)=true |
| demo5b | context.WithCancelCause/Cause(go1.20 release notes) | Cause(ctx)=disk full,同时 ctx.Err()=context canceled |
| demo6 | fan-in merge 收口:WaitGroup.Wait() 后 close(out),通道自然关闭(pipelines 的 merge 模式) | 收到 2 个值,range 正常结束 |
(c)HTTP 优雅退出对照实验(shutdowndemo,两次运行,日志时间戳为证):
- 运行 1:handler
select监听r.Context().Done(),且BaseContext返回主信号 ctx。/__quit(调用stop(),与信号同一条 context 取消路径)后日志:
04:50:13 quit requested; calling stop() (same path as signal)
04:50:13 shutdown signal received (ctx.Err=context canceled, cause-channel closed)
04:50:13 /work aborted: context canceled
04:50:13 server exited gracefully
in-flight 请求被中止——信号 ctx 经 BaseContext 传给每个请求,r.Context() 在信号瞬间取消,会读 ctx 的 handler 选择放弃。
- 运行 2(对照组):handler
/workhard无视请求 ctx,坚持 sleep 1.5s 后写响应。信号后:
04:50:34 quit requested; calling stop() (same path as signal)
04:50:34 shutdown signal received (ctx.Err=context canceled, cause-channel closed)
04:50:35 server exited gracefully
响应 done-hard 在信号之后仍被送达(curl 实收),进程在 in-flight 完成后才退出——与 RudderStack 引述的 Shutdown 语义("wait[s] indefinitely for connections to return to idle")一致,也直接暴露了"BaseContext 挂信号 ctx + handler 尊重 r.Context()"会与"让 in-flight 跑完"互相矛盾这一设计张力。
(d)证据充分性评估:核心机制的文献证据(官方一手)+ 本地复现(6/6 通过)+ 优雅退出两分支实测,链条完整。未覆盖项见「6. 局限」。
5. 结论
Go 并发模型可以压缩成一组显式契约,四个关键词各管一段生命周期:
- goroutine:廉价(M:N 多路复用到 OS 线程、栈按需伸缩、可开几十万)但不是免费的且不被 GC——"Goroutines are not garbage collected; they must exit on their own"。因此每个
go func()的成立前提是存在确定的退出口。 - channel:所有权转移式的通信原语;close 是唯一的广播机制(接收已关闭 channel 立即得零值),发送已关闭 channel 是 panic。由此推出所有权分工:发送方负责 close,接收方永不 close;多发送者时由第三方(
WaitGroup.Wait()后的 goroutine)收口。 - context:把 done channel 模式标准化成树——取消沿树向下传播,
cancel是父节点的特权、监听Done()是子节点的义务;WithValue只许携带请求域数据。它解决的是 pipelines 文章里"每个接收方要数清上游发送者"的不可扩展问题。 - 错误处理:错误是值,必须显式搬运(跨 goroutine 只能走 channel 或 errgroup)、显式聚合(Go 1.20
errors.Join/多重%w+errors.Is/As穿透)、显式归因(WithCancelCause/Cause区分"取消了"和"为什么取消")。 - 优雅退出:
signal.NotifyContext把不可广播的 OS 信号变成可广播的 context 取消 → 停生产者 →WaitGroup/Shutdowndrain 消费者与 in-flight →WithTimeout限时兜底强杀。http.Server.Shutdown的"先关 listener、再关 idle、等活跃回 idle"语义保证了 drain 的正确性——但本会话实测证明:若把信号 ctx 设为BaseContext且 handler 尊重r.Context(),in-flight 会在信号瞬间被应用层自己取消;"优雅"要靠 handler 自己选择忽略取消或另挂底座 ctx,Shutdown只是提供了等待的骨架。
一句话:Go 把并发正确性从"运行时替你兜底"改写为"程序员签一组显式契约"——每个 goroutine 有退出路径、close 权归发送方、取消沿 ctx 树传播、错误跨边界必须显式携带、退出前必须 drain。
6. 局限
来源自认的局限:
- Pipelines 文中的手工 done channel 方案,作者自己承认中间形态脆弱:"The choice of buffer size of 1 here depends on knowing the number of values merge will receive… This is fragile"。
- 2014 年两篇博文的示例 API 已陈旧(
ioutil.ReadFile、http.DefaultClient、Google Web Search API 已下线);博文模式的原始形态(手工传递 done)在工程里已被errgroup.WithContext与signal.NotifyContext吸收,照抄会写出"比现代写法更啰嗦且易错"的代码。 - RudderStack 文章代码样本有笔误(本会话 WebFetch 摘读时确认:select 里的
break、signal.Notify引用未定义变量c),作者示例需对照其配套 repo 校验后才可用。 - RudderStack 作者承认
Setup()/Shutdown()组件模式的风险:"callers forgetting to callShutdown"。
我(精读者)补充推断的局限:
errors.Join不携带堆栈,消息只是err1\nerr2拼接;生产排查通常还要配 slog/堆栈方案——此判断基于我的既有知识,未在本次来源中复核。- 本次未验证:
goleak泄漏检测、go test -race、GOMAXPROCS 与容器 CPU limit 的交互、errgroup(需golang.org/x/sync依赖,本地 demo 刻意只用标准库)。 - demo6 的 fan-in 把
sq退化为直通(c1/c2 是同一个 channel),只验证了 merge 收口结构,未验证完整的"平方 + fan-out + fan-in"链路。 signal.NotifyContext的真实信号注册部分在 Windows Git Bash 下无法可靠注入 SIGINT,改用stop()走同一取消路径替代——即信号→ctx 这最后一跳是"同构推断"而非直接实测;在 Linux/K8s 环境下行为未验证。- 七维里的"对我的启发"部分(下节)是我个人的迁移推断,超出八篇来源的文本范围。
7. 对我的启发
- 先答"它怎么退出",再写
go func(){}。每个 goroutine 必须挂在这三样之一上:父辈要 close 的 done/ctx、要 Wait 的 WaitGroup/errgroup、或进程生命周期本身。没有退出口 = demo3 那个永不回收的泄漏。这条也可以直接迁移到我的 agent/工作流编排:每个子任务启动前先定义取消通道。 - 取消的所有权是反转的:
cancel是发送方特权,Done()是接收方义务——Context刻意不给Cancel方法就是把这个不对称写进类型系统。写库代码时,函数收到 ctx 只许监听、不许替调用方取消。 - 错误处理三层于心:
fmt.Errorf("...: %w", err)加上下文 →errors.Is/As让上游按行为决策(重试/降级)→ 并发场景errors.Join/errgroup 聚合。服务侧再补一层WithCancelCause,把"context canceled"这种无信息量日志升级成"为什么取消"。 - 优雅退出模板(signal → ctx → 停生产 → drain → 限时强杀)是我后续任何 Go 常驻服务的 main 骨架;本次实测的
BaseContext张力直接给出一条配置铁律:别把可取消的信号 ctx 直接当BaseContext,除非接受"信号即中止 in-flight"的语义;要"跑完再走"就把底座 ctx 与信号 ctx 分开,drain 期间让 handler 继续用信号前的请求 ctx。 - 版本红线:Go 1.22+ 的循环变量修复让
i := i成为历史,但只对 go.mod ≥1.22 的模块生效;审旧代码时该坑仍然真实存在(vet 的 loopclosure 检查也只在 ≥1.22 的文件上关闭)。 - 把博文当"原理源"而非"代码源":2010-2014 的官方博文讲的不变式(close 广播、泄漏、契约)今天仍然成立,但其示例代码要经过"现代标准库翻译"(done→ctx、ioutil→os、手工收口→errgroup)再用。
二、从机制直接推导:一套 Go 服务优雅退出骨架与踩坑清单
以下骨架综合 RudderStack 文章 + 官方语义整理;结构未整段在本地运行(errgroup 需golang.org/x/sync,本会话刻意只用标准库做分量验证),其中信号 ctx、Shutdown等待 in-flight、ErrServerClosed过滤三块已分别由 shutdowndemo 实测覆盖。
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // 信号到达或 main 返回都会触发取消(RudderStack §3)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error { // HTTP 服务
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err // ErrServerClosed 是正常退出信号,不算失败
}
return nil
})
g.Go(func() error { // 信号哨兵:ctx 取消后限时 Shutdown
<-gCtx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return srv.Shutdown(shutdownCtx) // 先关 listener → 关 idle → 等活跃回 idle
})
g.Go(func() error { // 生产者:监听 gCtx,取消即停
return producer.Run(gCtx)
})
// 消费者 drain:等生产者全退 → close(ch) → 消费者 range 到关闭自然收尾
// (顺序铁律:先停生产,再 close,最后等消费——反了会 panic 或丢数据)
if err := g.Wait(); err != nil {
log.Fatal(err) // errgroup 把第一个非 nil 错误带回 main,并自动取消 gCtx
}
}踩坑清单(每条对应上文证据):
os.Exit跳过所有 defer ≈ SIGKILL(RudderStack 原文);log.Fatal同样直接os.Exit(1),只会绕过优雅退出。signal.Notify的 channel 若不缓冲且信号密集,会丢信号;signal.NotifyContext免除该心智负担。srv.Shutdown不会主动断开活跃连接(语义是等回 idle),所以必须套超时 ctx 防止"永久等"。ListenAndServe在Shutdown后必返回http.ErrServerClosed——不过滤它,退出码永远是红的(shutdowndemo 已实测此过滤)。BaseContext挂信号 ctx ⇒ in-flight 请求的r.Context()信号即取消(运行 1 实测);想让请求跑完,底座 ctx 与信号 ctx 分离,或 handler drain 期间忽略r.Context()(运行 2 对照)。- 向已关闭 channel 发送 panic、读已关闭 channel 得零值(demo1/demo2)——所以"谁发送谁 close",多方发送用 WaitGroup 收口(pipelines merge 模式,demo6)。
- 缓冲为 1 的
errc技巧:错误 channel 只发一次且必须不被 select 卡住(pipelines:"No select needed here, since errc is buffered")。
三、重要原文引述索引(防丢失)
| # | 引述 | 出处 |
|---|---|---|
| 1 | "Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once." | https://go.dev/talks/2012/waza.slide slide 7 |
| 2 | "Goroutines are multiplexed onto OS threads as required. When a goroutine blocks, that thread blocks but no other goroutine blocks." | 同上 slide 31 |
| 3 | "It's routine to create thousands of goroutines in one program. (Once debugged a program after it had created 1.3 million.) … Goroutines aren't free, but they're very cheap." | 同上 slide 34 |
| 4 | "Do not communicate by sharing memory; instead, share memory by communicating." | https://go.dev/blog/codelab-share (引 Effective Go) |
| 5 | "Goroutines are not garbage collected; they must exit on their own." | https://go.dev/blog/pipelines |
| 6 | "Sends on a closed channel panic, so it's important to ensure all sends are done before calling close." | 同上 |
| 7 | "a receive operation on a closed channel can always proceed immediately, yielding the element type's zero value" | 同上 |
| 8 | "stages close their outbound channels when all the send operations are done. / stages keep receiving values from inbound channels until those channels are closed or the senders are unblocked." | 同上(管道守则) |
| 9 | "A Context does not have a Cancel method … the function receiving a cancellation signal is usually not the one that sends the signal." | https://go.dev/blog/context |
| 10 | "when a Context is canceled, all Contexts derived from it are also canceled." / "A Context is safe for simultaneous use by multiple goroutines." | 同上 |
| 11 | "At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests." | 同上 |
| 12 | "Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions." | https://pkg.go.dev/context |
| 13 | "It is the error implementation's responsibility to summarize the context. The error returned by os.Open formats as 'open /etc/passwd: permission denied,' not just 'permission denied.'" | https://go.dev/blog/error-handling-and-go |
| 14 | "The language's design and conventions encourage you to explicitly check for errors where they occur (as distinct from … throwing exceptions and sometimes catching them)." | 同上 |
| 15 | "The new function errors.Join returns an error wrapping a list of errors." / "errors.Is and errors.As … inspect multiply wrapped errors." | https://go.dev/doc/go1.20 |
| 16 | "WithCancelCause … cancel a context with a given error. That error can be retrieved by calling the new Cause function." | 同上 |
| 17 | "In Go 1.22, each iteration of the loop creates new variables, to avoid accidental sharing bugs." | https://go.dev/doc/go1.22 |
| 18 | "Go channels do not work in a broadcast way, only one go routine will receive a single os.Signal." | https://www.rudderstack.com/blog/implementing-graceful-shutdown-in-go/ |
| 19 | Shutdown 语义:"first clos[es] all open listeners, then clos[es] all idle connections, and then wait[s] indefinitely for connections to return to idle." | 同上 |
| 20 | os.Exit during running goroutines ≈ "essentially equal to SIGKILL, no chance for closing open connections." | 同上 |
四、本会话执行记录(可复现)
环境与命令均在 Windows 10.0.26200 x64 / Git Bash / go1.24.5 下执行;代码位于 C:\Users\马鑫悦\.zcode\workspace\draft-438a36ca-1ee9-4ad0-9f51-71a4e441e47e\go-concurrency-demo\。
$ go version
go version go1.24.5 windows/amd64
$ cd go-concurrency-demo && go vet ./... && go run .
demo1: 接收已关闭 channel 立即返回, 耗时=0s, 类型=struct {}
demo2: 向已关闭 channel 发送 panic -> send on closed channel
demo3: goroutine 数 before=1 after=2 (泄漏 goroutine 仍存活)
demo4: 每次迭代独立循环变量 -> [0 1 2]
demo5a: errors.Is(joined, errA)=true, errors.Is(joined, errB)=true
demo5b: context.Cause(ctx)=disk full, ctx.Err()=context canceled
demo6: fan-in 收到 2 个值(期望2), 通道自然关闭, 无泄漏优雅退出实验(go build -o shutdowndemo.exe ./shutdowndemo 后按「4.(c)」的两轮脚本驱动):
运行1(handler 监听 r.Context(),BaseContext=信号ctx):
04:50:13 quit requested; calling stop() (same path as signal)
04:50:13 shutdown signal received (ctx.Err=context canceled, cause-channel closed)
04:50:13 /work aborted: context canceled ← in-flight 被应用层取消
04:50:13 server exited gracefully
运行2(/workhard 无视请求 ctx):
04:50:34 quit requested; calling stop() (same path as signal)
04:50:34 shutdown signal received (ctx.Err=context canceled, cause-channel closed)
04:50:35 server exited gracefully ← 等 in-flight 完成后才退出
curl 实收 in-flight 响应:done-hard ← Shutdown 等待语义得证未运行项(诚实清单):errgroup 版骨架整段未运行(缺 golang.org/x/sync);真实 SIGINT 注入未测(Windows Git Bash 限制);go test -race、goleak 未运行。
*笔记完。本文档由主题精读流程生成于 2026-11-03;只新增本文件,未改动其他条目笔记、matrix.csv、backlog 或 nightly-log.md。*