钩子必须在依赖就绪后注册,即在main()中完成log.init()、db.connect()、config.load()等初始化后再注册;启动钩子需在http.listenandserve()前执行,关闭钩子须配合signal.notify和srv.shutdown()顺序执行。

钩子必须在依赖就绪后注册,不能放 init()
Go 微服务里最常见的 panic 就是把数据库迁移、日志初始化这类逻辑塞进 init(),结果钩子一执行就 panic:配置没加载、*sql.DB 是 nil、日志实例未构造。这不是钩子写得不对,而是注册时机错了。
真正安全的注册位置只有一个:在 main() 函数中,完成所有依赖初始化之后——比如 log.Init()、db.Connect()、config.Load() 全部返回成功之后,再往全局钩子切片里 append。
- 用
[]func()管理启动/关闭钩子,别搞 map 或嵌套结构体(初期没必要) - 如果用了 Wire/Dig 做依赖注入,钩子注册必须是最后一步,确保所有 provider 已就绪
- 单元测试容易骗人:
init()不报错不代表集成环境能跑通;务必在本地启一个完整服务验证
启动钩子要放在 http.ListenAndServe 之前
启动钩子(比如预热缓存、上报健康状态、触发一次定时任务)不是“服务起来后再执行”,而是在 HTTP server 启动前、所有依赖已可用时立即运行。否则你可能看到服务端口已监听,但内部状态还没 ready,下游调用直接失败。
典型错误写法是把钩子调用写在 go srv.ListenAndServe() 之后——这时主 goroutine 已继续往下走,甚至可能已经退出,钩子根本没机会执行。
- 正确顺序:
registerHooks()→runStartupHooks()→http.ListenAndServe() - 钩子函数签名统一为
func(),有上下文需求就用闭包捕获(如func() { db.Ping() }) - 多个钩子按注册顺序串行执行;若需并发或加锁,自己显式控制,GORM 或标准库不帮你做
关闭钩子必须配合 signal.Notify + Shutdown 顺序执行
收到 SIGTERM 后钩子没执行,90% 是因为没监听信号,或者 srv.Shutdown() 阻塞了主线程,导致后续钩子全被跳过。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
关键点在于:关闭流程不是“收到信号→执行钩子→退出”,而是“收到信号→异步关 server→同步执行反注册/DB.Close/GRPC.Stop→最后 os.Exit”。任意环节卡住或顺序颠倒,都会导致幽灵实例或数据丢失。
- 必须调用
signal.Notify(c, os.Interrupt, syscall.SIGTERM),Go 默认不转发信号 -
srv.Shutdown(ctx)要放新 goroutine,主 goroutine 立即进入 deregister → gRPC.GracefulStop → db.Close 流程 - Consul 反注册必须在
srv.Shutdown(ctx)返回之后、os.Exit(0)之前;绝不能写defer,kill -9会跳过 - 每个网络调用(如反注册 HTTP 请求)都要设超时、重试、失败只 log,不阻断后续步骤
避免用 slice 索引隐式约定执行顺序
靠 startupHooks[0] 表示“最先执行”、shutdownHooks[2] 表示“最后执行”,这种硬编码顺序在多人协作或模块拆分后极易出错。上线后新加一个监控上报钩子,不小心插在中间,就可能导致 DB 还没关、日志 writer 却先停了。
真要控制顺序,就用带权重的结构体封装,而不是靠数组下标猜意图。
- 定义
type Hook struct { Name string; Phase string; Priority int; Enabled bool; Fn func() } - 注册时按
Phase(如"pre-start","post-stop")分组,再按Priority排序执行 - 禁用某个钩子只需设
Enabled: false,不用删代码或注释掉 slice 元素
复杂度其实不高,但能提前避开后期维护时最让人头疼的“这个钩子到底该在哪执行”的争论。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










