必须显式调用signal.notify并正确配置才能捕获sigint/sigterm,否则进程直接终止;需在主业务启动前注册、明确指定os.interrupt和syscall.sigterm、使用带缓冲通道并持续读取。

signal.Notify 必须显式注册才能捕获 SIGINT/SIGTERM
不调用 signal.Notify,Go 程序对 SIGINT(Ctrl+C)和 SIGTERM(kill -15)完全无感——内核直接终止进程,清理逻辑一概不执行。这不是“没写好”,而是 Go 的默认行为:信号不监听就不管。
常见错误包括:
- 只传
os.Interrupt,漏掉syscall.SIGTERM—— Kubernetes、systemd、Docker stop 默认发的就是它 - 把
signal.Notify放在http.Serve之后,导致信号在监听前已到达并被丢弃 - 用
signal.Notify(c)不带具体信号参数,结果只监听了默认的SIGINT和SIGQUIT,依然漏SIGTERM
正确写法是:signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM),且必须在启动 HTTP server 或长期循环之前调用。
通道必须带缓冲且持续读取,否则信号会丢失
make(chan os.Signal, 1) 是底线配置。无缓冲通道在第一次信号到来后就会阻塞,第二次 kill -15 或快速连按 Ctrl+C 时,信号直接被运行时丢弃,毫无日志、无报错、无声无息。
更关键的是 goroutine 必须持续消费:
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
- 错误写法:
sig := 只取一次,后续信号全丢 - 正确写法:用
for range sigChan或for { 循环读取 - 缓冲设为 1 足够;设更大(如 10)反而掩盖问题——比如本该阻塞等待 shutdown 完成,却因缓冲“吞掉”二次信号而误判流程正常
Shutdown 超时不是等时间,而是防卡死
http.Server.Shutdown 不会强制中断正在跑的 handler,它只是关 listener + 等所有活跃请求自然返回。如果某个 handler 里调了没 timeout 的 http.Client.Do、没 context 的 db.QueryRow,或有个 for range ch 忘记监听 ctx.Done(),它就会一直卡住,直到你设的超时触发强制关闭(Close),此时连接可能中断在半途。
实操建议:
- 超时设 10–30 秒,别用 5 秒——长轮询、文件上传、慢查询都撑不住
- 所有外部调用必须显式带 context:
client.Do(req.WithContext(ctx))、db.QueryRowContext(ctx, ...) - 别在
Shutdown前调srv.Close()—— 这会提前断开 listener,但已有连接仍可继续处理;Shutdown自己会做这一步
本地测试时怎么确认信号真被收到
按 Ctrl+C 后程序退出,不代表 shutdown 流程执行了。可能因为 panic 恢复干扰、select 没覆盖到信号 channel、或 goroutine 已退出,导致 sigChan 虽然收到信号,但没人消费。
验证方法很简单:
- 在
case sig := 分支开头加一行 <code>log.Println("got signal:", sig) - 在
srv.Shutdown调用前后也打日志,例如log.Println("starting shutdown...") - 用
kill -15 $(pidof yourapp)测试,比 Ctrl+C 更贴近生产环境 - 观察日志顺序:是否先打出 signal 日志,再打出 shutdown 开始日志,最后有结束日志?缺任何一环都说明流程断在中间
最容易被忽略的是:main goroutine 不能在启动 signal 监听后就 return。它必须阻塞住(比如 select {}),否则整个进程立刻退出,shutdown 根本没机会开始。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










