正确做法是gin路由层仅做协议转换与jwt校验,调用本地service方法,由grpc client通过服务发现调用其他服务,避免http直连;proto需用google.protobuf.doublevalue等显式可选字段防止null隐式转换;本地开发用docker-compose按service名通信并读环境变量,各服务独立管理go.mod。

直接结论:用 Gin + gRPC + Protocol Buffers 拆分服务边界,HTTP 层只暴露 RESTful 资源接口,内部通信走 gRPC,别在 HTTP handler 里直连其他服务的数据库或 HTTP 接口。
很多人一上来就用 net/http 手写路由、拼 JSON、自己解析 query,结果订单服务调商品服务时硬编码 http://product-svc:8080/products/123,一升级路径或加鉴权就全崩——这不是微服务,是“微耦合单体”。下面说清楚怎么真正落地。
拆服务前先定好资源路径和 HTTP 动词语义
RESTful 不是加个 /api/v1 就算数。电子商城核心资源必须对齐业务实体,且动词不可滥用:
-
GET /api/v1/users/{id}→ 返回单个用户(含关联的收货地址、最近订单 ID 列表),不返回完整订单详情 -
POST /api/v1/orders→ 创建订单,body 必须含user_id、items(含product_id和quantity),不接受product_name这类冗余字段 -
GET /api/v1/orders/{id}/items→ 单独拉订单明细,不和订单主数据混在一个接口里 - 禁止
POST /api/v1/orders/cancel这种非资源路径;应为PUT /api/v1/orders/{id}+ body{"status": "cancelled"}或更推荐DELETE /api/v1/orders/{id}(若业务允许逻辑删除)
动词错位会导致前端反复适配、缓存失效、无法被 OpenAPI 工具识别。Gin 的 router.GET、router.POST 等方法名就是强制你思考语义,别全写成 router.Any。
Gin 路由层只做协议转换,不做跨服务编排
常见错误:在 order_handler.go 里用 http.Client 去调 user-svc 和 product-svc,再把结果 merge 成一个 JSON 返回。这会带来三个硬伤:
- 超时不可控:一个下游慢,整个请求卡住
- 错误传播:商品服务返回 503,订单接口也跟着 503,前端无法区分是下单失败还是查商品失败
- 可观测性归零:链路追踪断在 HTTP 调用处,看不到 gRPC 内部耗时
正确做法是:Gin handler 只负责解析请求、校验 JWT token、调用本地 order_service.CreateOrder() 方法;这个方法内部用 gRPC client 同步或异步调 UserClient.GetUser() 和 ProductClient.GetProducts() —— 服务发现、重试、熔断都交给 gRPC 的拦截器处理,不是手写 if err != nil { time.Sleep(...) }。
Proto 定义要约束字段可空性与边界,别让 JSON 隐式转义毁掉一致性
比如商品服务的 ProductService.proto 中,如果写成:
message Product {
int32 id = 1;
string name = 2;
double price = 3;
}
问题来了:price 是 double,但前端传 "price": null 或 "price": "",gRPC 解析直接 panic;而 HTTP 层用 Gin 绑定 JSON 时,json.Unmarshal 会静默设为 0,导致价格变成 0 元上架。
必须改用显式可选字段:
message Product {
int32 id = 1;
string name = 2;
google.protobuf.DoubleValue price = 3; // 来自 google/protobuf/wrappers.proto
}
这样:nil 表示未提供,0.0 表示明确设为 0,Gin 层接收到 JSON 后先转成 proto message 再传给 service,避免 “null 被吞”、“空字符串变 0” 这类隐式转换陷阱。
本地开发时用 docker-compose 启动多服务,别依赖 localhost 端口硬编码
新手常把 user-svc 的 gRPC 地址写死成 localhost:9001,结果一进容器就 dial tcp 127.0.0.1:9001: connect: connection refused。docker-compose.yml 必须定义清晰的 service 名和端口映射:
services:
user-svc:
build: ./user
ports: ["9001:9001"]
order-svc:
build: ./order
environment:
- USER_SVC_ADDR=user-svc:9001
depends_on: [user-svc]
代码里读环境变量:os.Getenv("USER_SVC_ADDR"),而不是拼字符串。同时在 main.go 初始化 gRPC client 时加上 grpc.WithBlock() 和合理超时,避免启动时卡死。
最易被忽略的一点:每个服务的 go.mod 必须单独管理依赖,user-svc 用 github.com/gin-gonic/gin v1.9.1,order-svc 用 v1.10.0 没问题——微服务本就不该共享同一份 go.mod。强行统一版本,迟早因某个服务升级 Gin 导致所有服务一起改。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











