根本原因是fd未传递、子进程抢shutdown、主goroutine提前退出;必须提前ignore sigusr2防干扰、初始化channel再调shutdown、阻塞主goroutine等待完成,并通过extrafiles安全传递listener fd。

Go 里用 signal.Notify 注册了 SIGUSR2 却没反应,或者一重启就报 bind: address already in use,根本不是信号没收到或代码写错了——是 fd 没传过去、子进程抢着 shutdown、主 goroutine 提前退出这三件事漏了一个。
为什么 signal.Notify 注册了却收不到 SIGUSR2
常见错觉是“注册了就一定能收到”,但实际在 fork 子进程后,SIGUSR2 默认会传播到所有子进程(比如用 exec.Command 启动的新实例),导致新老进程同时收到信号,互相干扰甚至一起关 listener。
- 必须在子进程启动前调用
signal.Ignore(syscall.SIGUSR2),否则新进程一启动就触发 shutdown,和父进程抢 listener -
signal.Notify的 channel 必须在http.Server.Shutdown()调用前就初始化好;如果信号来了但 channel 还没 ready,信号就丢了 - macOS 不支持用户进程接收
SIGUSR2(报operation not permitted),开发环境建议统一改用SIGHUP或SIGTERM
http.Server.Shutdown() 总是超时失败?
不是函数有问题,而是你没控制住它依赖的上下文链路。Shutdown() 只负责停止 accept 新连接 + 等待已有连接 close,但它不等你业务里没设 timeout 的 HTTP 调用或 DB 查询。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
- 所有外部调用必须带 context:用
http.Client.Do(req.WithContext(ctx)),DB 查询用db.QueryContext(ctx, sql) - 超时不能硬写 5 秒——长轮询、WebSocket、大文件上传可能卡住;建议设 10–30 秒,并加日志:
log.Printf("shutdown took %v", time.Since(start)) -
Shutdown返回后,别急着os.Exit;要确保所有后台 goroutine(比如 metrics 上报、日志 flush)也通过同一 context cancel 掉
父子进程间怎么安全传递监听 socket 的 fd
真正平滑的关键不是 Go 代码,而是父子进程间传递 socket fd。Go 标准库不直接暴露 fd 传递逻辑,得靠 syscall + os/exec 配合环境变量或 Unix domain socket 中转。
- 父进程收到信号后,调
ln.(*net.TCPListener).File()获取*os.File,记下file.Fd()(通常是 3) - 启动子进程时,在
os.StartProcess的sys.ProcAttr.Files中填入[]uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), 3} - 子进程启动后第一件事:读
os.Getenv("LISTEN_FDS")和os.Getenv("LISTEN_PID")做校验,再用os.NewFile(3, "")恢复*os.File,再用net.FileListener(file)转成net.Listener - systemd 启动时默认不开启
FileDescriptorStoreMax=,需在 service 文件里显式加FileDescriptorStoreMax=1
最难的不是写几行 signal.Notify,而是让所有 handler 都响应 ctx.Done()、让 fd 在父子间不被意外关闭、让主 goroutine 真正等到 shutdown 完成才退出——这三个点任何一个松动,就会丢请求或卡死。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










