grpc流式通信由.proto文件中stream关键字位置决定:服务端流为rpc x() returns (stream y),客户端流为rpc x(stream y) returns (z),双向流为rpc x(stream y) returns (stream z);必须用--go-grpc_out=.生成接口,且send()/recv()需手动错误检查与循环调用。

流式通信不是配个选项或加个 flag 就能开,而是由 .proto 文件里 stream 关键字的位置和写法决定的——写错、漏写、或生成命令少一个插件,后续所有代码都跑不起来。
proto 文件里 stream 怎么写才对
gRPC 不区分“开启流模式”,只认 stream 出现在请求类型或响应类型前的位置。它不是修饰符,是语法硬性要求。
-
rpc GetLogs(LogRequest) returns (stream LogResponse)→ 服务端流(客户端发一次,服务端回多次) -
rpc Upload(stream Chunk) returns (UploadResult)→ 客户端流(客户端发多次,服务端回一次) -
rpc Chat(stream Message) returns (stream Message)→ 双向流(双方随时收发) - 别写成
rpc Bad(stream LogRequest) returns (LogResponse):既不是标准客户端流(缺stream在返回),也不符合 gRPC 接口契约,生成代码会缺失关键方法 -
message必须显式定义,不能用string或bytes直接当参数;repeated bytes data是高危写法,容易触发 OOM,应改用单次bytes data+int64 offset分块设计
生成 Go 代码时最容易漏掉的两件事
只运行 protoc --go_out=. 是不够的,它只生成消息结构,不生成流式接口所需的 Server/Client 类型。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 必须同时指定
--go-grpc_out=.,否则编译会报undefined: pb.XXXServer - 必须安装两个插件:
protoc-gen-go和protoc-gen-go-grpc;用go install google.golang.org/protobuf/cmd/protoc-gen-go@latest和go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - 生成命令示例:
protoc --go_out=. --go-grpc_out=. --go-grpc_opt=paths=source_relative streaming.proto - 检查生成结果:服务端流对应的是
XXX_YYYServer类型(如StreamService_ServerStreamServer),不是*grpc.ServerStream—— 手动传错类型会导致函数根本不会被调用
服务端实现流方法时最常卡死的三个地方
函数签名看着简单,但实际运行中很容易在某次 Send() 就永久阻塞,不是逻辑错,而是没处理好流的生命周期。
-
Send()是同步写入 HTTP/2 流的,不加错误检查就循环调用,一旦客户端断开或网络卡住,goroutine 就 hang 住,表现为 CPU 低但连接数持续上涨 - 务必每次
Send()后检查错误:if err := stream.Send(&res); err != nil { return err };漏掉这步,一次失败后继续下轮,最终在已关闭的 stream 上 panic - 别复用同一个
message实例反复改字段再Send():proto 序列化内部 buffer 会被覆盖,客户端收到的可能是上一轮的数据 - 函数签名只能有一个
stream参数(在请求参数之后),不能额外加context.Context—— 要用stream.Context() - 不要依赖
defer“兜底”发送 ——return即结束流,defer来不及执行
客户端读流必须手动循环调用 Recv()
很多人以为能像读 channel 一样 for range client,但 client.Recv() 不是迭代器,它每次只取一个消息,不循环就只读第一条。
- 标准写法就是:
for { res, err := stream.Recv(); if err != nil { break }; handle(res) } -
err == io.EOF表示服务端正常结束流;其他非nil错误(比如网络断开)要主动处理,不能忽略 - 双向流客户端必须启两个 goroutine:一个发、一个收;单线程串行读写必然卡死
- 发数据前先检查
stream.Context().Err() == nil,避免往已失效的 stream 写入引发 panic - 别在
Send()后立刻Recv()—— 这不是请求响应模型,两边完全异步
真正难的不是写几行 Send() 和 Recv(),而是理解每个流对象自带完整生命周期、上下文和错误语义;一旦把流当成普通 channel 或 callback 处理,问题就会藏得深、复现难、排查久。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










