1. 为什么内部用 gRPC
JSON/HTTP 给人读方便,但服务之间调用量大、要强类型、要高性能。gRPC 用 protobuf 二进制编码,体积小、序列化快,自带强类型契约和流式。HTTP 给前端,gRPC 给内部。
2. 定义 proto
syntax = "proto3";
package user;
service UserService { rpc GetUser(GetUserReq) returns (GetUserResp); }
message GetUserReq { string id = 1; }
message GetUserResp { string id = 1; string name = 2; int32 age = 3; }
protoc 或 buf 生成 Go 代码,客户端服务端共用,契约一处定义。
3. 下游 gRPC 服务
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserReq) (*pb.GetUserResp, error) { u, err := s.repo.Find(req.Id) if err != nil { return nil, status.Error(codes.NotFound, "user not found") } return &pb.GetUserResp{Id: u.ID, Name: u.Name, Age: u.Age}, nil }
错误用 status.Error(codes.xxx, msg),对应 HTTP 状态码,前端能映射。
4. Gin 里调 gRPC 客户端
conn, _ := grpc.Dial("user-svc:8081", grpc.WithInsecure()) // 生产用 TLS + 服务发现 client := pb.NewUserServiceClient(conn) func (h *OrderHandler) Create(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 500*time.Millisecond) defer cancel() u, err := client.GetUser(ctx, &pb.GetUserReq{Id: uid}) if err != nil { c.JSON(502, gin.H{"error": "用户服务不可用"}) return } }
连接池复用 conn,别每次 Dial。超时用 context,防下游慢拖垮自己(第 3 篇讲过)。
5. 错误码映射
gRPC 状态码转成前端能懂的 HTTP:
if s, ok := status.FromError(err); ok { switch s.Code() { case codes.NotFound: c.JSON(404, gin.H{"error": "用户不存在"}) case codes.Unavailable: c.JSON(502, gin.H{"error": "用户服务不可用"}) } }
6. 连接管理
长连接要保活、要限流。用 grpc.WithKeepaliveParams 保活,客户端连接放全局单例。多实例靠服务发现拿地址(第 6 篇)。
7. 上线清单
proto 当契约,改动要兼容(字段只加不删)。连接全局复用,别每次 Dial。下游调用必带 context 超时。gRPC 错误码映射到 HTTP 状态码。服务间通信走内网,开 mTLS 别裸奔。流式场景(推送、大数据)用 gRPC stream 比轮询好。
下一篇讲事件驱动,用消息队列让服务彻底解耦。