不能直接用net/http轮询服务列表,因为服务发现需动态感知实例上下线,轮询会错过瞬时变更、增加中心压力且缺乏健康检查;应解耦discovery(支持watch接口)与selector,用sync.rwmutex安全更新内存缓存,并通过if-none-match/etag优化http请求。

为什么不能直接用 net/http 轮询服务列表
因为服务发现不是静态配置同步,而是动态感知实例上下线。直接轮询会错过瞬时变更、增加中心节点压力,且无法做健康检查兜底。真正要解决的是:如何让客户端在不依赖外部代理(如 Envoy)的前提下,自主完成服务名 → 可用实例地址的实时映射。
关键点在于把“发现”和“选择”解耦:Discovery 负责拉取/监听变更,Selector 负责从实例列表里挑一个。两者通过接口隔离,后续可替换 Consul/Etcd/Nacos 实现,不影响业务调用逻辑。
-
Discovery接口必须支持Watch()方法,返回chan []Instance,而不是单次Get() - 实例结构体至少含
ID、Addr、Metadata(用于权重、区域标签等),避免硬编码字段 - 初始化时需设置超时与重试策略——很多开发者漏掉
context.WithTimeout,导致 Watch 协程卡死在阻塞读
如何用最小代码实现基于 HTTP 的服务发现客户端
假设后端是自建的轻量注册中心(如用 Gin 暴露 /v1/services/{name}/instances 接口),不需要 SDK,纯 net/http + time.Ticker 就够用。
重点不是“怎么发请求”,而是“怎么安全更新内存缓存”。常见错误是直接赋值 discovery.instances = newInstances,导致并发读写 panic。必须用指针原子更新或 sync.RWMutex。
- 用
sync.RWMutex保护实例切片,读多写少场景下比sync.Mutex更高效 - Watch goroutine 中每次收到新列表,先深拷贝再替换,避免外部修改影响内部状态
- HTTP 请求必须带
If-None-Match和Etag支持 304 缓存,否则每秒都触发全量响应 - 示例片段:
func (d *HTTPDiscovery) Watch(ctx context.Context, service string) chan []Instance { ch := make(chan []Instance, 1) go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case
Selector 怎么选实例才不至于负载倾斜
随机选(rand.Intn)看似简单,但实例上线/下线时容易集中打到少数节点;轮询(RoundRobin)又缺乏故障熔断能力。轻量级方案里,推荐“带健康标记的加权随机”。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
核心是把实例健康状态(如上次心跳是否超时)和权重(来自 Metadata)一起参与计算,而不是仅靠地址哈希或序号。
- 每次 Select 前先过滤掉
Healthy == false的实例,避免无效重试 - 权重不是固定数值,而是动态衰减:连续失败 3 次,权重降为 1;恢复成功后线性回升
- 不要在 Selector 里做重试逻辑——那是调用方或中间件的事,Selector 只负责“给一个能用的地址”
- 示例判断逻辑:
if inst.Healthy && inst.Weight > 0 { candidates = append(candidates, inst) }
为什么 context.Context 必须传进 Watch 和 Select
不是为了“符合 Go 风格”,而是解决两个真实问题:服务关闭时协程泄漏、测试时超时难控制。没有 context 的 Watch 会永远阻塞在 http.Client.Do 或 chan 读取上。
尤其注意:Watch 返回的 chan []Instance 不是无限流,它背后有 http 请求生命周期。一旦 context cancel,必须主动关闭该 chan 并退出 goroutine,否则泄漏。
- 所有对外 I/O 操作(HTTP 请求、DNS 解析、etcd Watch)都必须绑定传入的
ctx - Select 方法也需接收
ctx,用于控制单次选择的等待上限(比如最多等 200ms 找不到健康实例就报错) - 测试时用
context.WithTimeout(testCtx, 500*time.Millisecond),能快速暴露未处理 cancel 的 goroutine
最易被忽略的是 Watch 内部的 HTTP client 超时设置——它和 context 超时是两层,client 超时控制单次请求,context 控制整个 Watch 生命周期。两者缺一不可。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










