
多工具扇出最容易写出一种“看起来已经收尾”的代码:外层收到超时就返回,所有子调用拿到了同一个 context,结果通道也加了缓冲。于是注释写上“绝不泄漏”。问题是,子调用若在一个不响应 context 的函数里等待,外层再完整的取消逻辑也不能把它强行拉出来。
Go 的 context 文档明确说,CancelFunc 不等待工作停止。Go 1.27 的运行时改进也没有改变这个接口契约。排查 Agent 工作者时,要把“调用者离开”“任务收到停止信号”“任务实际退出”记成三个事件。
缓冲通道只能解决一段阻塞
容量等于任务数的结果通道,可以容纳每个任务的一次结果,避免无人接收时卡在发送上。但它不能解决任务内部的数据库等待、无限循环或不带取消能力的 SDK 调用。若另外启动一个 goroutine 等待 WaitGroup,它也可能跟着等待。
下面把生命周期句柄显式返回。发送阶段监听取消;done 只在工作者真正结束后关闭。代码保存为 lifecycle_test.go,在独立目录运行 GO111MODULE=off go test -race。
package lifecycle
import (
"context"
"testing"
"time"
)
func Start(ctx context.Context, fn func(context.Context) string) (<-chan string, <-chan struct{}) {
out := make(chan string)
done := make(chan struct{})
go func() {
defer close(done)
defer close(out)
value := fn(ctx)
select {
case out <- value:
case <-ctx.Done():
}
}()
return out, done
}
func waitDone(t *testing.T, done <-chan struct{}) {
t.Helper()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("worker did not finish")
}
}
func TestCooperative(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, done := Start(ctx, func(ctx context.Context) string {
<-ctx.Done()
return "stopped"
})
cancel()
waitDone(t, done)
}
func TestCancellationIsNotForcedExit(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
started, release := make(chan struct{}), make(chan struct{})
_, done := Start(ctx, func(context.Context) string {
close(started)
<-release // 故意不响应取消的反例;测试最后一定释放。
return "late result"
})
<-started
cancel()
select {
case <-done:
t.Fatal("blocked function cannot already be finished")
default:
}
close(release)
waitDone(t, done)
}

第二个测试没有制造永久泄漏:先证明取消不能穿透不合作的函数,再主动解除阻塞并等待退出。两秒只是测试失败保护,不是退出延迟的性能承诺。本地 Go 1.26.5 的 race 测试通过;未做 Go 1.27 压测,也没有模型 SDK 或真实网络参与。
退出责任要落到具体资源
HTTP 调用应把 context 传到请求,连接读写要有可解除阻塞的关闭或 deadline;长期循环要在合适粒度检查取消。对无法取消的第三方调用,应限制并发与最长持有成本,必要时用可终止的独立进程隔离。进程隔离也不能撤销已经提交到外部的业务动作。
done 超过清理预算仍未关闭时,应把它记为“尚未退出”,保留任务标识、阻塞堆栈与资源归属。不要为了让监控曲线好看而把计数提前减掉。判断泄漏需要负载回落后的持续观测与堆栈证据,单张 goroutine 数量图不能证明所有任务安全结束。
参考:context 取消契约、Go 1.27 发布说明。