服务已运行时长应通过全局变量 starttime = time.now() 在 main() 开头记录,并用 time.since(starttime) 安全获取;/health 接口需区分 /live(进程存活)和 /ready(依赖就绪),避免使用 c.request.host 判定状态,且健康检查路由不应挂载 logger 或 recovery 中间件。

如何用 time.Since() 获取服务已运行时长
服务启动时间不是靠“读系统时间再减”这种手动计算,而是用 Go 原生的 time.Time 类型配合 time.Since() 安全获取。关键在于:启动时刻必须在 main() 入口处记录一次,且不能被并发修改。
- 在
main()函数开头定义全局变量:var startTime = time.Now() - 提供一个 HTTP 接口(如
/health)返回运行时长:c.JSON(200, gin.H{"uptime": time.Since(startTime).String()}) - 不要在 handler 里重复调用
time.Now()再减——纳秒级精度下,两次调用之间可能有微小偏差;time.Since()内部使用单调时钟,更可靠 - 如果项目用了多实例或热重载(如 air),注意
startTime是进程级的,每次重启都会重置
为什么 c.Request.Host 不能代表服务状态
很多开发者误以为检查 c.Request.Host 或 c.Request.URL.Path 就能判断服务是否“健康”,其实这只是请求层面的字段,和后端真实状态无关。真正的服务状态应反映依赖可用性、内部资源水位等。
-
c.Request.Host来自 HTTP 请求头,可被任意伪造,完全不可信 - 单纯返回
{"status": "ok"}没有意义——数据库连不上、Redis 超时、队列积压时,它依然会返回 200 - 生产环境的
/health必须做轻量级探活:比如 ping 本地 MySQL 连接池、检查 RedisPING响应、确认核心 goroutine 是否卡死 - 建议把检查逻辑封装成函数,例如
checkDB() error,在 health handler 中逐个调用并聚合结果
gin.Default() 自带的中间件不适用于健康检查
gin.Default() 注入了 gin.Logger() 和 gin.Recovery(),它们对普通接口是必要的,但对 /health 这类高频探测接口反而有害。
- Logger 会写日志——每秒几十次健康检查会让日志文件暴涨,还可能拖慢响应
- Recovery 在 panic 时兜底,但健康检查本身不该 panic;若因检查逻辑出错 panic,反而掩盖了真正的问题
- 正确做法是为 health 路由单独建一个 engine 实例,或用
r.NoRoute()/r.Any()配合gin.BasicAuth()保护,且不挂任何中间件 - 示例:
health := r.Group("/health", gin.DisableConsoleColor()),然后health.GET("/live", liveHandler)和health.GET("/ready", readyHandler)
区分 /live 和 /ready 的实际意义
Kubernetes 等编排系统要求明确区分存活探针(liveness)和就绪探针(readiness),Gin 路由必须对应实现,不能混用一个 endpoint。
-
/live只检查进程是否存活:能响应 HTTP 即可,不查下游依赖;失败则重启容器 -
/ready检查是否可接收流量:必须验证 DB、Redis、消息队列等关键依赖;失败则从 Service 后端摘除,但不重启 - 两者返回状态码不同:
/live失败返回 500,/ready失败返回 503;K8s 依据此码决策 - 别在
/ready里加耗时操作(如全量缓存预热),它应在毫秒级返回;复杂初始化应放在启动阶段完成
/health 接口,比没有还危险。











