根本原因是main函数返回即进程终止,goroutine被强制回收;须用sync.WaitGroup显式跟踪写任务并配合context.WithTimeout设超时,HTTP服务需用Shutdown而非Close。

主函数退出时 goroutine 被强杀,写操作中断的根本原因
Go 程序中 main 函数一返回,整个进程立即终止,所有正在运行的 goroutine(包括正在执行 write、db.Exec、http.Post 的)都会被操作系统强制回收。这不是超时或错误,是直接 kill —— 文件可能只写了一半,数据库事务没提交,HTTP 响应头已发但 body 被截断。
关键不是“阻止退出”,而是让 main 多等一会儿,且等得有依据:等写操作真正完成,而不是靠猜时间。
用 sync.WaitGroup + defer wg.Done() 显式跟踪写任务
适用于明确知道写任务数量或能提前注册的场景(如启动 N 个日志写入 goroutine、批量插入数据库)。
-
wg.Add(1)必须在go语句之前调用;写在 goroutine 内部会因竞态导致漏注册,main提前退出 - 每个写操作 goroutine 入口第一行就写
defer wg.Done(),哪怕后面立刻return或panic也能保证计数减一 -
main结尾必须调wg.Wait(),它会阻塞直到所有Done()执行完毕 - 别用
time.Sleep替代wg.Wait():写磁盘慢了 200ms 就丢数据,快了又白等 5 秒
示例:
func main() {
var wg sync.WaitGroup
wg.Add(2)
<pre class="brush:php;toolbar:false;">go func() {
defer wg.Done()
// 模拟写文件
f, _ := os.Create("log.txt")
f.Write([]byte("start\n"))
time.Sleep(300 * time.Millisecond) // 实际可能是 io.Copy
f.Write([]byte("done\n"))
f.Close()
}()
go func() {
defer wg.Done()
// 模拟 DB 写入
db.Exec("INSERT INTO events ...")
}()
// 等两个写操作都真正结束
wg.Wait()}
用 context.WithTimeout 给写操作设硬性截止时间
当写操作可能卡死(如网络抖动、磁盘满、DB 锁表),光靠 WaitGroup 会永久 hang 住。必须配合 context.Context 主动超时控制。
- 创建带超时的 context:
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second),8 秒是经验值,按业务最长容忍延迟定 - 把
ctx传给所有可取消的写操作:如os.File.Write不支持 ctx,但http.Client.Do(req.WithContext(ctx))、db.QueryContext(ctx, ...)、io.CopyN(dst, src, n)可结合ctx.Done()检查 -
cancel()应在wg.Wait()前调用,确保 goroutine 收到信号后开始收尾 - 注意:
context.WithCancel本身不带超时,必须用WithTimeout或WithDeadline防死锁
HTTP server 或长连接场景下,写响应体中断怎么防?
直接调 http.Server.Close() 会立刻断开 TCP 连接,客户端收不到完整响应体。Go 1.8+ 的 Shutdown 是唯一正确解法。
- 不要用
srv.Close(),改用srv.Shutdown(ctx) -
ctx必须带超时,例如context.WithTimeout(context.Background(), 10*time.Second) -
Shutdown会先关闭 listener,再等待已有连接完成响应(包括正在写 body 的) - 若超时后仍有连接未关,
Shutdown返回context.DeadlineExceeded,此时可强制srv.Close()
典型组合:
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<p>go func() {
</p><p>srv.ListenAndServe()</p>
最易被忽略的一点:写操作是否真正“完成”,不取决于你调了 Write,而取决于底层是否 flush 并确认送达。比如 net/http 的 responseWriter 默认缓冲,WriteHeader 后 Write 的内容可能还在内存里——Shutdown 能等它刷出去,但裸 io.Writer 需手动 Flush() 或用带 context 的封装。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











