Go gRPC
在 Go 中使用 gRPC:Protobuf 契约、四种 RPC 模式、代码生成、拦截器、status 错误码、与 HTTP/JSON 边界。
[!info] Related notes
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. 初始化与生成
在新的示例目录中初始化模块,把上方契约保存到 hello/v1/greeter.proto。先安装 protoc(本次验证用 33.0),并确保两个 Go 插件所在的 GOBIN 或 GOPATH/bin 在 PATH 中。以下为已验证的版本组合,不代表必须使用这些版本:
go mod init example.com/hello
go get google.golang.org/grpc@v1.83.2 google.golang.org/protobuf@v1.36.11
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.1
protoc -I . \
--go_out=. --go_opt=module=example.com/hello \
--go-grpc_out=. --go-grpc_opt=module=example.com/hello \
hello/v1/greeter.proto
参考:gRPC Go Quickstart、protobuf Go。
生成文件会位于 gen/hellov1/,与 go_package 和下面的 import 一致。paths=source_relative 则跟随输入目录,在本例会写入 hello/v1/,不能直接与当前 import 混用。
3. 服务端 cmd/greeter-server/main.go
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) {
// 检查入口时的取消;实际耗时工作也应接收 ctx 并响应取消
if err := ctx.Err(); err != nil {
return nil, status.FromContextError(err).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")
if err := s.Serve(lis); err != nil {
log.Fatal(err)
}
}
4. 客户端 cmd/greeter-client/main.go
package main
import (
"context"
"fmt"
"log"
"time"
hellov1 "example.com/hello/gen/hellov1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func run() error {
// 明文本地演示;部署时按实际链路配置 TLS 身份验证。
conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return err
}
defer conn.Close()
client := hellov1.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
resp, err := client.SayHello(ctx, &hellov1.HelloRequest{Name: "go"})
if err != nil {
return err
}
fmt.Println(resp.GetMessage())
return nil
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
运行:
go mod tidy
# 终端 1:
go run ./cmd/greeter-server
# 终端 2:
go run ./cmd/greeter-client
正常输出 hello go;空 name 得 InvalidArgument。本例未启用 server reflection;使用 grpcurl 时要提供契约:
grpcurl -plaintext -import-path . -proto hello/v1/greeter.proto \
-d '{"name":"go"}' localhost:50051 hello.v1.Greeter/SayHello
结合场景再看三个关注点
- 按值嵌入
UnimplementedGreeterServer:重新生成代码增加方法后,旧实现源码仍可编译,新方法默认返回 Unimplemented。 - 业务错误用
status.Error(codes.XXX, ...)。 - 确认传输与身份验证边界:直接暴露的 RPC 配置 TLS,按需要使用 mTLS;若由 sidecar/代理终止 TLS,仍需明确其到应用的链路与信任范围。
核心概念与准确模型
四种 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)分类处理
普通非 status 错误通常被映射为 Unknown,不应据此做业务分类;可将 context 错误显式转换为 status.FromContextError(err).Err()。DeadlineExceeded/Unavailable 不证明远端写入没有发生,重试需结合幂等性。
拦截器 ≈ 中间件
grpc.NewServer(grpc.UnaryInterceptor(loggingInterceptor))
鉴权、超时、指标、tracing;流式有独立 interceptor API。
元数据与 context
- 默认没有应用级 deadline;调用方设置
context.WithTimeout,剩余期限经 RPC 传播,服务端需把 context 继续传给耗时操作 - 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. ClientConn 与负载均衡
grpc.NewClient(v1.63+)只创建逻辑 channel,不做网络 I/O;首次 RPC 自动触发连接,构造成功不代表服务可用。WithBlock 等旧 Dial 选项对 NewClient 无效。ClientConn 可并发复用,进程退出时关闭;它可能管理多个底层连接,HTTP 本身也支持长连接。负载均衡、重试与退避策略需根据 resolver、部署与幂等性配置。
4. 上下文取消
服务端必须听 ctx.Done(),否则客户端已走仍占资源。
常见误区
[!warning] 常见误区:生产使用 insecure 凭证
内网也建议 mTLS 或至少 TLS。
[!warning] 常见误区:返回裸
errors.New当业务分类
客户端无法稳定映射码;用 status。
[!warning] 常见误区:忽略
Unimplemented*Server
按默认生成规则,缺少嵌入可能导致接口实现编译失败;嵌入解决新增方法后的源码兼容。旧二进制不会因此自动获得新 RPC,新方法通常返回 Unimplemented。
[!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()等待在途 RPC,长流可能使其一直等待;设置关闭期限,到期调用s.Stop()强制终止。
可验证实验
2026-10-07:Go 1.26.8、protoc 33.0、protoc-gen-go v1.36.11、protoc-gen-go-grpc v1.6.1、grpc-go v1.83.2 下生成路径与两个程序编译通过;bufconn 测试正常/空参数、未知方法、超时取消与 NewClient 无 I/O,go test -race ./... 通过。真实 TLS、负载均衡与滚动发布需另做部署验证。
实验 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。
- 重新生成接口新增方法时,嵌入为尚未实现的方法提供 Unimplemented 默认实现,主要保证源码层面的前向兼容。
代码题:无法稳定区分;应
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 | 本库 | 取消、分层、错误 |