Go gRPC
在 Go 中使用 gRPC:Protobuf 契约、四种 RPC 模式、代码生成、拦截器、status 错误码、与 HTTP/JSON 边界。
[!info] 关联笔记
Go gRPC
这个概念为什么会出现
服务间若用临时 JSON + 手写路径:
- 契约易漂(字段类型、必填)
- 流式与双向通信别扭
- 多语言客户端重复造轮子
gRPC 用 Protocol Buffers 定义服务与消息,基于 HTTP/2 做多路复用,并生成强类型客户端/服务端桩代码。Go 是 gRPC 一等公民(google.golang.org/grpc)。
[!abstract] 一句话理解 先写
.proto契约,再生成 Go 接口;服务端实现接口,客户端像调本地方法一样发 RPC;错误用 status code,横切用拦截器,超时用 context。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:用户服务间 Unary RPC——SayHello 问候
两个微服务:网关把“打招呼”转给用户服务。
契约用 .proto 固定;生成 Go 桩后:
- 服务端实现
Greeter.SayHello - 空
name返回InvalidArgument - 客户端像调本地方法一样发 RPC
依赖:google.golang.org/grpc + 生成的 *.pb.go(第三方/生成代码保留)。
1. 契约 hello/v1/greeter.proto
syntax = "proto3";
package hello.v1;
// go_package:生成包路径与包名
option go_package = "example.com/hello/gen/hellov1;hellov1";
// Greeter:服务间问候 RPC
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1; // 字段号 1 稳定,勿乱改
}
message HelloReply {
string message = 1;
}
2. 生成(示意,以当前插件文档为准)
protoc -I . \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
hello/v1/greeter.proto
参考:gRPC Go Quickstart、protobuf Go。
3. 服务端
package main
import (
"context"
"fmt"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
hellov1 "example.com/hello/gen/hellov1"
)
// server:实现生成的 GreeterServer。
// 嵌入 Unimplemented*:新加 RPC 方法时旧实现仍可编译(前向兼容)。
type server struct {
hellov1.UnimplementedGreeterServer
}
// SayHello:Unary——一请求一响应。
func (s *server) SayHello(ctx context.Context, req *hellov1.HelloRequest) (*hellov1.HelloReply, error) {
// 尊重取消/超时(客户端 WithTimeout 会传到这里)
if err := ctx.Err(); err != nil {
return nil, err
}
if req.GetName() == "" {
// 业务可预期失败 → status code,不是随便 Unknown
return nil, status.Error(codes.InvalidArgument, "name required")
}
return &hellov1.HelloReply{Message: "hello " + req.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
// 注册实现到 gRPC 运行时
hellov1.RegisterGreeterServer(s, &server{})
fmt.Println("gRPC :50051")
log.Fatal(s.Serve(lis))
}
4. 客户端片段
// 本地演示可用 insecure;生产务必 TLS / mTLS
conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
// 旧代码可能是 grpc.Dial
client := hellov1.NewGreeterClient(conn)
// 像本地调用:传入 ctx 与请求消息
resp, err := client.SayHello(ctx, &hellov1.HelloRequest{Name: "go"})
// resp.GetMessage() == "hello go"
建议运行(需先 protoc 生成并配置 module 路径):
# 终端 1:起服务
go run ./cmd/greeter-server
# 终端 2:用 grpcurl 或自写 client 调 SayHello
期望:name=go 时响应 message: "hello go";空 name 得 InvalidArgument。
结合场景再看三个关注点
- 嵌入
UnimplementedGreeterServer保证前向兼容。 - 业务错误用
status.Error(codes.XXX, ...)。 - 生产必须 TLS/mTLS;insecure 仅本地演示。
核心概念与准确模型
四种 RPC 模式
| 模式 | 特征 | 用途 |
|---|---|---|
| Unary | 一请求一响应 | 大多数 API |
| Server streaming | 一请求多响应 | 订阅、大列表分块 |
| Client streaming | 多请求一响应 | 上传聚合 |
| Bidirectional | 双流 | 聊天、实时同步 |
代码生成角色
| 文件 | 内容 |
|---|---|
*.pb.go | 消息类型 |
*_grpc.pb.go | Client/Server 接口与注册 |
业务只实现 Server 接口,不手写线格式。
错误模型
google.golang.org/grpc/status + codes:
InvalidArgument/NotFound/PermissionDenied/Unavailable…- 客户端
status.FromError(err)分类处理
不要把一切变成 Unknown。
拦截器 ≈ 中间件
grpc.NewServer(grpc.UnaryInterceptor(loggingInterceptor))
鉴权、超时、指标、tracing;流式有独立 interceptor API。
元数据与 context
- 超时:
context.WithTimeout经 RPC 传播 - metadata 传 trace id、auth token(注意日志脱敏)
与 HTTP JSON
| gRPC | JSON/HTTP | |
|---|---|---|
| 契约 | .proto 生成 | OpenAPI/手写 |
| 浏览器 | 需网关/grpc-web | 原生友好 |
| 流 | 一等 | SSE/WebSocket 另建 |
对外公网常 JSON;服务间 gRPC。可用 grpc-gateway 等双暴露。
设计动机
- 契约优先:字段编号稳定演化。
- 性能与流式:HTTP/2 + 二进制。
- 多语言一致客户端。
- 与 Go 接口、context 模型契合。
边界情况与反直觉行为
1. 字段编号不可乱改
兼容靠 field number;改号 = 事故。保留 reserved。
2. proto3 默认值
标量默认 0/"";区分“未设置”需 optional 或包装类型。
3. 长连接与负载均衡
与短连接 HTTP 思维不同;需客户端 LB、重试与退避策略。
4. 上下文取消
服务端必须听 ctx.Done(),否则客户端已走仍占资源。
常见误区
[!warning] 常见误区:生产使用 insecure 凭证
内网也建议 mTLS 或至少 TLS。
[!warning] 常见误区:返回裸
errors.New当业务分类
客户端无法稳定映射码;用 status。
[!warning] 常见误区:忽略
Unimplemented*Server
新加 RPC 时旧二进制行为异常。
[!warning] 常见误区:在 server 实现里堆领域+SQL
应适配到 service 层,同 HTTP 分层。
与相邻概念对比
| 概念 | 差异 |
|---|---|
| REST/JSON | 人类友好;契约与流式工具链不同 |
| GraphQL | 客户端选字段;非二元 RPC |
| 消息队列 | 异步解耦;gRPC 多为在线调用 |
| SSE | 浏览器友好单向流;非通用 RPC |
工程实践
- buf 或脚本固化生成,CI 检查 generated 一致。
- 鉴权:metadata Bearer + interceptor(理念同 JWT)。
- 可观测:OpenTelemetry 拦截器。
- 测试:
bufconn内存监听,无需真端口。 - 版本:
package hello.v1+ 目录/模块路径版本化。 - 优雅停止:
s.GracefulStop()与进程信号结合。
可验证实验
实验 1:Unary
空 name → InvalidArgument;正常 name → message。
实验 2:超时
服务端 sleep,客户端短 timeout → DeadlineExceeded。
实验 3:拦截器
记录 full method 与耗时。
实验 4:生成物
改 proto 加字段,确认旧客户端仍可(兼容规则内)。
本节总结
- 本质:IDL + 生成代码 + HTTP/2 上的 RPC 栈。
- 关键规则:契约演化、status 错误、ctx 取消、拦截器横切、TLS。
- 最易错:明文生产、改 field number、胖 server、裸 error。
- 下一步:鉴权;流式对比 SSE;复用 service。
自测题
概念题
- gRPC 常见序列化与传输是什么?
- 四种 RPC 模式中,服务端持续推送更贴近哪种?
- 为什么嵌入
UnimplementedXxxServer?
代码推理题
服务端 return fmt.Errorf("not found"),客户端如何区分 NotFound?
工程思考题
已有 JSON HTTP,新内部调用上 gRPC,如何避免两套业务逻辑?
参考答案
展开
- Protobuf;HTTP/2。
- Server streaming。
- 为未实现的新方法提供默认行为,利于二进制兼容。
代码题:无法稳定区分;应status.Error(codes.NotFound, ...)。
工程题:抽出 service;HTTP handler 与 gRPC server 双适配器。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| gRPC Go Quickstart | 官方 | 生成与 Hello World |
| google.golang.org/grpc | 包文档 | Server/Client |
| Protocol Buffers | 官方 | 语言与演化 |
| Status codes | 文档 | 错误语义 |
| go-context · go-http-handler-service-repository · go-error-handling | 本库 | 取消、分层、错误 |