标准库 net/http 可构建轻量服务发现中心,但需解耦注册/发现/健康检查;consul 注册须在监听启动后执行,健康检查路径需显式注册,客户端应独立配置并缓存结果,http 与 grpc 服务须分开注册与发现,注销需主动确认。

标准库 net/http 足够搭一个轻量服务发现中心,但别直接暴露 Consul API 或硬编码注册逻辑——核心是把“注册/发现/健康检查”三件事解耦成可独立启动、可测试、无全局状态的组件。
Consul 注册必须等服务真正监听后再触发
常见错误是 init() 里就调用注册,结果 http.ListenAndServe 还没跑起来,Consul 健康检查直接失败,服务被秒剔除。更糟的是,有些框架(如 microg)默认不注册 /healthz,你得自己加 handler。
- 注册动作必须放在
Server.Run()成功返回监听地址之后,比如在ln, err := net.Listen(...)后立即执行 - 健康检查路径要显式注册:用独立
http.ServeMux实例,只挂/healthz,不走业务中间件 - Consul 的
DeregisterCriticalServiceAfter建议设为30s,对应心跳间隔15s,留出网络抖动余量
服务发现客户端不能依赖全局单例
多个微服务共用一个 consulapi.Client 实例时,超时、重试、连接池配置容易互相污染;更麻烦的是,不同服务对同一服务名的缓存策略可能冲突。
- 每个服务应创建自己的
consulapi.Client,通过api.DefaultConfig()配置专属Address和HttpClient - 查询实例时用
client.Health().Service(<code>serviceName, "", true, &q),第三个参数true表示只返回通过健康检查的节点 - 避免在 handler 里实时查 Consul——高频调用会拖慢响应;改用本地缓存 +
time.Ticker定期刷新,缓存失效时间建议设为 Consul 检查间隔的 2 倍
HTTP 和 gRPC 服务注册要分开建模
同一个服务同时暴露 HTTP 和 gRPC 接口时,不能复用同一组 Consul 注册 ID 或健康检查路径——gRPC 的健康检查是 grpc.health.v1.Health/Check,HTTP 是 GET /healthz,协议层根本不互通。
- HTTP 服务注册用
AgentServiceCheck.HTTP字段,gRPC 服务注册用AgentServiceCheck.TCP+ 端口,或单独起一个 HTTP 代理转发健康检查请求 - Consul 中注册两个不同
ID:比如user-http-1和user-grpc-1,避免注销时误删另一协议实例 - 客户端发现时按协议类型过滤:HTTP 调用查
ServiceName+ taghttp,gRPC 查 taggrpc,不要混用
优雅注销必须处理信号并等待注册中心确认
进程收到 SIGTERM 后直接退出,Consul 不会立刻感知,僵尸服务残留长达 DeregisterCriticalServiceAfter 时间(默认 30s),期间新请求仍会被路由过去。
- 注册时记录返回的
serviceID,注销时用它调用client.Agent().ServiceDeregister(<code>serviceID) -
Server.Shutdown()后,加一段阻塞等待:循环调用client.Health().Service()直到返回空列表或超时(建议 5s) - 别依赖 Consul 的自动剔除机制——它只是兜底,主动注销才是可控手段
最难的不是写注册代码,而是让每个服务实例对自己的生命周期有明确边界:谁注册、谁心跳、谁注销、谁缓存、谁超时。这些逻辑一旦散落在各处,调试时连日志都对不上时间线。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











