第3章 goroutine 和资源必须有人负责到底
go 语句只负责启动
启动 goroutine 的语法没有返回句柄,没有默认的父子关系,也没有自动 join。创建它的函数返回之后,它仍可能继续运行。因此每个 go f() 都应能回答四个问题:谁让它停止,谁等待它结束,错误交给谁,使用的资源由谁释放。如果这些答案都散落在注释里,代码结构通常需要调整。
可以把所有权理解为最终清理责任,而不是独占使用权。一个共享 Transport 可以供很多请求并发使用,但服务负责在不再需要时关闭空闲连接。一个任务可以被 worker 执行,工作池仍负责记录它的终结并等待 worker。通过参数传递指针不会自动转移责任,转移必须发生在明确的接纳动作上。
随书 /tasks 接收的是短小任务描述。请求参数经校验后构造成独立数据,工作池接纳成功才成为后台任务;提交失败则由请求直接返回错误。不能把 *http.Request、请求 Body 或 ResponseWriter 放到后台队列里。处理函数返回后,这些对象的协议生命周期已经结束,后台继续使用既难以推理,也容易出现竞争。
借用、共享与转移
借用意味着使用时间不能超过拥有者的范围。处理函数同步读取请求头属于借用;在后台保存请求头 map 的引用则延长了借用,通常应该复制需要的字段。共享意味着多个执行者共同访问同一对象,需要同步和关闭协议。转移意味着接收方接管清理,发送方不能再按原责任关闭它。
这些区分直接影响 channel 的设计。如果通过 channel 发送缓冲区,发送的是 slice 描述符,不是底层字节的深拷贝。生产者复用缓冲区时,消费者可能正在读取同一片内存。可以选择复制数据,也可以约定消费完成后归还缓冲区,但必须让归还成为协议的一部分。对象池提供复用能力,不提供这种所有权证明。
文件、响应体、计时器和网络连接也一样。最贴近创建动作的作用域通常适合安排 defer;一旦资源跨越函数边界,就要写清由谁关闭。返回 io.ReadCloser 的函数是在把关闭责任交给调用者。返回完整字节的函数则可以在内部关闭响应体,接口更简单,但必须限制读取大小。
等待结束比发出取消更重要
cancel() 表示请求停止,Wait() 表示确认结束,两者不可互换。服务退出时只广播取消然后直接返回 main,后台日志、文件 flush 或任务状态可能尚未完成。随书工作池把停止接纳、等待任务和最终取消分开,让测试能分别验证每个阶段。
WaitGroup 是等待工具,不能存储错误,也不能阻止新任务加入。通常应在启动 goroutine 前完成 Add,避免它先执行 Done 或 Wait 提前观察到零。若允许 Add 与关闭并发,就需要额外接纳门闩,使“允许加入”和“开始等待”具有明确顺序。把 WaitGroup 本身当作工作池状态机会产生难以复现的关闭竞态。
等待结束也不能无限进行。顶层需要截止时间,并在超时后记录尚未完成的责任。对协作式函数,服务取消能让它们退出;对完全不合作的代码,进程终止可能是唯一边界。这个事实应体现在接口契约与测试中,而不是被“优雅退出”四个字遮住。
错误的归属也是资源管理
后台 goroutine 里的错误不会自动返回给启动者。如果只是打印,调用方可能仍认为任务成功;如果把错误发送到无人接收的 channel,它又会变成泄漏。工作池需要决定哪些错误只终结当前任务,哪些错误应让整个服务失败,并保留可查询或可观测的结果。
panic 也不能无条件吞掉。对隔离的用户任务,可以在清晰边界 recover 并记录堆栈、标记失败;对破坏内部不变量的 panic,继续服务可能比退出更危险。随书工程不鼓励把任意错误包装成成功,只要求任何受控失败路径都完成计数与资源清理。生产扩展时应围绕真实故障域决定恢复策略。
goroutine 数是有用信号,但它不是所有权证明。HTTP 连接、运行时和诊断工具都会创建 goroutine,测试前后数量不一定精确相等。更稳健的断言是拥有的 worker 已退出、被阻塞的操作返回、后台任务 active 归零;HTTP handler 则另用返回后的完成通知或 DrainRequests 确认。然后辅以 goroutine profile 检查异常堆栈。不要用一次全局计数相等替代这些证据。
练习:写一张责任表
在实验工程里寻找每一个 go、NewTimer、NewRequestWithContext 和 Close。为每项写下创建者、借用者、取消者、关闭者和等待者。若某一格填写“系统会处理”,继续追问是哪一个具体函数。再增加一个故意不检查取消的工作,观察 Shutdown 的截止能够保护等待者,却不能神奇地终止函数。
最后移除这个故障工作,检查所有测试仍然通过。练习不是证明 Go 不可靠,而是认识到服务保证来自整条调用链的合作。你写出的责任表应足以让另一个人指出:在哪个函数返回之后,哪些资源绝不应继续存在。
本章实测证据
提交成功后取消调用者,已接纳工作仍由池负责;关闭池后等待Done,确认责任转移与结束等待。 以下摘自最终本机 go test -race -v ./...,完整命令及环境见附录B;括号中的耗时仅是当次运行记录。
--- PASS: TestAcceptedTaskOutlivesCaller (0.00s)