← [ BACK_TO_NOTES ]

Go gRPC

在 Go 中使用 gRPC:Protobuf 契约、四种 RPC 模式、代码生成、拦截器、status 错误码、与 HTTP/JSON 边界。

#concept #growing #backend #go

[!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 桩后:

  1. 服务端实现 Greeter.SayHello
  2. 空 name 返回 InvalidArgument
  3. 客户端像调本地方法一样发 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

结合场景再看三个关注点

  1. 按值嵌入 UnimplementedGreeterServer:重新生成代码增加方法后,旧实现源码仍可编译,新方法默认返回 Unimplemented。
  2. 业务错误用 status.Error(codes.XXX, ...)。
  3. 确认传输与身份验证边界:直接暴露的 RPC 配置 TLS,按需要使用 mTLS;若由 sidecar/代理终止 TLS,仍需明确其到应用的链路与信任范围。

核心概念与准确模型

四种 RPC 模式

模式特征用途
Unary一请求一响应大多数 API
Server streaming一请求多响应订阅、大列表分块
Client streaming多请求一响应上传聚合
Bidirectional双流聊天、实时同步

代码生成角色

文件内容
*.pb.go消息类型
*_grpc.pb.goClient/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

gRPCJSON/HTTP
契约.proto 生成OpenAPI/手写
浏览器需网关/grpc-web原生友好
流一等SSE/WebSocket 另建

对外公网常 JSON;服务间 gRPC。可用 grpc-gateway 等双暴露。

设计动机

  1. 契约优先:字段编号稳定演化。
  2. 性能与流式:HTTP/2 + 二进制。
  3. 多语言一致客户端。
  4. 与 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

工程实践

  1. buf 或脚本固化生成,CI 检查 generated 一致。
  2. 鉴权:metadata Bearer + interceptor(理念同 JWT)。
  3. 可观测:OpenTelemetry 拦截器。
  4. 测试:bufconn 内存监听,无需真端口。
  5. 版本:package hello.v1 + 目录/模块路径版本化。
  6. 优雅停止: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。

自测题

概念题

  1. gRPC 常见序列化与传输是什么?
  2. 四种 RPC 模式中,服务端持续推送更贴近哪种?
  3. 为什么嵌入 UnimplementedXxxServer?

代码推理题

服务端 return fmt.Errorf("not found"),客户端如何区分 NotFound?

工程思考题

已有 JSON HTTP,新内部调用上 gRPC,如何避免两套业务逻辑?

参考答案

展开
  1. Protobuf;HTTP/2。
  2. Server streaming。
  3. 重新生成接口新增方法时,嵌入为尚未实现的方法提供 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本库取消、分层、错误

创建于 2026/6/25 更新于 2026/10/7
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本