推荐用顶层 platform 模块统一管理依赖版本,各服务通过 replace 指向对应路径,避免 grpc-go 等版本冲突;gin + gRPC 混合暴露接口,共用 proto 生成双端代码,并用 grpc-gateway 集成 HTTP 转发;服务注册发现必须用 etcd/consul,实现自动注册续约与健康监听;配置中心采用 viper + etcd 异步热更新,fallback 本地配置防启动阻塞。

用 go mod 初始化服务并统一管理依赖版本
微服务不是堆砌多个 main.go 就完事,第一步是让每个服务能独立构建又共享基础能力。直接在每个服务根目录执行 go mod init example.com/user-service 会埋下版本混乱的坑——比如 grpc-go 在 user-service 里用 v1.50,在 order-service 里却拉了 v1.62,接口不兼容就报 undefined: grpc.UnaryInterceptor。
推荐做法是建一个顶层 go.mod(比如叫 platform),里面只写 module platform 和 go 1.21,然后所有服务通过 replace 指向它:
module platform go 1.21 replace example.com/user-service => ./services/user replace example.com/order-service => ./services/order
这样既能用 go build ./services/... 一键编译全部服务,又避免各服务自己 go get 出冲突版本。
用 gin + gRPC 混合暴露接口,别只选一种
HTTP API 给前端或第三方调用,gRPC 给内部服务间通信——这是最常见也最合理的分层。硬全用 gRPC,前端得接 grpc-web 和代理;全用 HTTP,跨服务调用没类型安全、性能差、链路追踪难。
立即学习“go语言免费学习笔记(深入)”;
关键点在于共用一套 proto 定义,生成双端代码:
-
protoc --go_out=. --go-grpc_out=. user.proto生成 gRPC server/client -
protoc --grpc-gateway_out=logtostderr=true:. user.proto生成 HTTP 转发器 - 用
gin启动 HTTP 服务,把 gateway 注册进去,gin的中间件(如鉴权、日志)就能复用
注意:gateway 默认不支持 POST body 解析复杂嵌套结构,遇到 invalid character '{' looking for beginning of value,得加 runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{OrigName: false, EmitDefaults: true})。
服务注册与发现必须用 etcd 或 consul,别手写心跳
本地跑两个 go run main.go 看起来能通,一上 K8s 就连不上,大概率是服务地址写死或靠 DNS 轮询。真实环境要求:服务上线自动注册、下线自动剔除、故障时能快速熔断。
etcd 是最轻量且 Go 原生支持的选择。用 go.etcd.io/etcd/client/v3 写个 Register 函数,核心就三步:
- 调
client.Put(ctx, key, value, clientv3.WithLease(leaseID))注册 - 起 goroutine 定期
client.KeepAlive()续约 - 监听
/services/前缀,用client.Watch()获取变更事件
别用内存 map 存节点列表——重启后服务就“消失”了;也别依赖 K8s Service 做服务发现,它不提供健康检查语义,pod 还在 running 但进程已卡死,流量照样打过去。
配置中心用 viper + etcd,但别让服务启动时阻塞等配置
把数据库地址、超时时间、开关项全写进 config.yaml 是反模式。配置要支持热更新,且不能因 etcd 临时不可用导致服务起不来。
viper 可以做到:
- 启动时 fallback 到本地
config.yaml,确保基本可用 - 另起 goroutine 异步监听 etcd 路径,变更时调
viper.ReadConfig(bytes)更新内存值 - 所有业务代码通过
viper.GetDuration("timeout.write")读取,而非全局变量
容易忽略的是:etcd watch 连接断开后不会自动重连,得手动捕获 context.Canceled 错误并重建 watch;另外,viper 的 WatchKey 不支持前缀监听,必须用 WatchPrefix 配合自定义回调解析子 key。









