Go gRPC

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

#type / concept #status / growing #tech / dev / backend #resource / go

[!info] 关联笔记

Go gRPC

这个概念为什么会出现

服务间若用临时 JSON + 手写路径:

  • 契约易漂(字段类型、必填)
  • 流式与双向通信别扭
  • 多语言客户端重复造轮子

gRPCProtocol 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. 生成(示意,以当前插件文档为准)

protoc -I . \
  --go_out=. --go_opt=paths=source_relative \
  --go-grpc_out=. --go-grpc_opt=paths=source_relative \
  hello/v1/greeter.proto

参考:gRPC Go Quickstartprotobuf 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

结合场景再看三个关注点

  1. 嵌入 UnimplementedGreeterServer 保证前向兼容。
  2. 业务错误用 status.Error(codes.XXX, ...)
  3. 生产必须 TLS/mTLS;insecure 仅本地演示。

核心概念与准确模型

四种 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) 分类处理

不要把一切变成 Unknown

拦截器 ≈ 中间件

grpc.NewServer(grpc.UnaryInterceptor(loggingInterceptor))

鉴权、超时、指标、tracing;流式有独立 interceptor API。

元数据与 context

  • 超时:context.WithTimeout 经 RPC 传播
  • 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. 长连接与负载均衡

与短连接 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

工程实践

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

自测题

概念题

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

代码推理题

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

工程思考题

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

参考答案

展开
  1. Protobuf;HTTP/2。
  2. Server streaming。
  3. 为未实现的新方法提供默认行为,利于二进制兼容。
    代码题:无法稳定区分;应 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/7/15