直接用os.exit()或http.server.close()会丢请求,因为前者强制终止所有goroutine、跳过defer和清理逻辑,后者立即关闭listener并硬杀活跃连接,导致请求中断、日志截断、事务不全、客户端收到connection reset by peer;而http.server.shutdown()配合context.withtimeout()才能先拒新连、再等待旧连接完成或超时,实现真正优雅退出。

为什么直接用 os.Exit() 或 http.Server.Close() 会丢请求
进程收到 SIGTERM 后立刻调 os.Exit(0),所有 goroutine 被强制终止,defer 全失效,正在写的日志截断、数据库事务回滚不完整、上传中的文件被 RST 中断。而 http.Server.Close() 会立即关闭 listener 并硬杀所有活跃连接,客户端看到 Connection reset by peer 或空响应——这不是退出,是猝死。
http.Server.Shutdown() 必须配超时 context.WithTimeout()
Shutdown() 本身不阻塞,它只是发通知;真正等连接结束靠你传进去的 context。不加超时,它可能永远卡住(比如 handler 里写了 time.Sleep(1h) 或没设 timeout 的 db.QueryRow())。
- 超时时间得略大于业务最长耗时:API 接口 5–10 秒,文件上传或 SSE 流式响应建议 15–30 秒
- 必须用
context.WithTimeout(context.Background(), 15*time.Second),不能用context.Background()或context.TODO() - 调用后要忽略
http.ErrServerClosed,其他错误该log.Fatal()就log.Fatal() -
defer cancel()别漏,否则 context 泄露
后台 goroutine 怎么同步响应退出信号
Go 没有强制 kill goroutine 的机制,所有长期运行逻辑(消息消费、定时任务、健康检查)必须主动监听 ctx.Done() 并在收到后立刻清理、返回。
- 别轮询
if ctx.Err() != nil { return }——这是忙等,且只查一次,容易错过信号 - 正确写法是
select { case ,清理逻辑必须写在 <code>case分支里 - HTTP handler 里的 I/O 调用(如
db.QueryRowContext()、client.Do(req.WithContext(ctx)))必须透传 context - 多个服务共存时,用同一个
context.WithCancel()创建的ctx驱动全部 goroutine,并用sync.WaitGroup等它们全部退出后再关 DB/Redis 连接池
信号监听为什么总收不到 SIGINT/SIGTERM
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 只是注册转发规则,不接收也不阻塞。如果注册完主 goroutine 就退出(比如没 for range sigChan 或 ),信号根本不会被处理。
- channel 必须带缓冲(如
make(chan os.Signal, 1)),防止信号丢失 - 注册后必须显式接收,常见写法是
sig := 或 <code>for range sigChan - 收到信号后,先
cancel()触发所有ctx.Done(),再wg.Wait(),最后才执行最终资源释放(如db.Close()) -
http.ListenAndServe()返回http.ErrServerClosed是正常现象,不是错误,要显式忽略
最常被忽略的一点:清理动作(删临时文件、关连接池、注销服务)必须发生在 ctx.Done() 触发之后、goroutine 返回之前;放在 defer 里等于没写——信号来了,goroutine 还在跑,defer 根本不执行。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











