推荐用顶层 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/clientprotoc --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)) 注册client.KeepAlive() 续约/services/ 前缀,用 client.Watch() 获取变更事件别用内存 map 存节点列表——重启后服务就“消失”了;也别依赖 K8s Service 做服务发现,它不提供健康检查语义,pod 还在 running 但进程已卡死,流量照样打过去。
viper + etcd,但别让服务启动时阻塞等配置把数据库地址、超时时间、开关项全写进 config.yaml 是反模式。配置要支持热更新,且不能因 etcd 临时不可用导致服务起不来。
viper 可以做到:
config.yaml,确保基本可用viper.ReadConfig(bytes) 更新内存值viper.GetDuration("timeout.write") 读取,而非全局变量容易忽略的是:etcd watch 连接断开后不会自动重连,得手动捕获 context.Canceled 错误并重建 watch;另外,viper 的 WatchKey 不支持前缀监听,必须用 WatchPrefix 配合自定义回调解析子 key。