.proto中stream位置错误会导致grpc流类型错乱:服务端流需写在returns侧,客户端流在参数侧,双向流两侧都要写;send()/recv()方法、接口签名及struct名均由此严格决定。

proto 中 stream 关键字写错位置,生成的 Go 接口就完全不能用
gRPC 流类型不是靠注释或文档约定,而是由 .proto 文件里 stream 出现在请求侧还是响应侧严格决定的。写错一个字,生成的 Go 接口方法签名、是否含 Send()/Recv()、甚至 struct 名都会错乱。
-
rpc GetLogs(LogRequest) returns (stream LogResponse)→ 服务端流,生成接口含Send(*LogResponse) error -
rpc Upload(stream Chunk) returns (UploadResult)→ 客户端流,生成接口含Recv() (*Chunk, error),不含Send() -
rpc Sync(stream Req) returns (stream Resp)→ 双向流,两端都有Send()和Recv() - 常见错误:
rpc Bad(stream Req) returns (Resp)看起来像服务端流,实际是客户端流;rpc Bad(Req) returns (Resp)漏掉stream,生成的是 unary 接口,根本没流方法
stream.Send() 不检查错误,第三次调用就卡死或静默失败
Go 的 gRPC stream 是同步阻塞调用,Send() 失败(比如连接已断、缓冲区满)不会自动重试,也不会抛 panic —— 错误被忽略后,下一次 Send() 很可能直接阻塞在底层 write 调用上,或返回 rpc error: code = Unavailable desc = transport is closing。
- 必须每次
Send()后显式判断:if err := stream.Send(&msg); err != nil { return err } - 不要复用同一个
*pb.Msg实例反复改字段再Send()—— proto 序列化复用内部 buffer,后一次覆盖前一次,客户端收到空或乱码 - 不要依赖
defer在函数退出时“补发”数据,RPC 生命周期由调用栈控制,return就终止流 - 数据库游标或文件读取场景下,确保最后一次
Send()在io.EOF之前完成,否则末尾数据丢失
客户端 Recv() 写成 for range 或固定次数,导致连接提前关闭
stream.Recv() 不是 channel,不能 for range;也不是无界循环,不加 io.EOF 判断会一直阻塞,直到上下文超时或连接被中间件(如 Envoy/Nginx)静默断开。
- 标准读法必须是:
for { res, err := stream.Recv() if err != nil { if err == io.EOF { break } return err } handle(res) } - 别写
for i := 0; i 这种固定次数逻辑 —— 服务端可能发 12 条,最后两条永远收不到 - 双向流中,收发要分离到不同 goroutine,避免单协程里
Recv()卡住导致无法Send() - 如果服务端发送间隔长(如日志推送每 5s 一条),记得设足够长的 context timeout,否则
Recv()直接返回context.DeadlineExceeded
大文件传输必设 grpc.MaxRecvMsgSize 和 grpc.MaxSendMsgSize
gRPC 默认限制单条消息 4MB,os.ReadFile() → proto.Bytes → stream.Send() 在低内存环境(如容器或嵌入式设备)必然 OOM;Nginx/Envoy 默认 60s 超时、HTTP/2 流控僵死也会让传输卡在 70%。
- 服务端和客户端都需显式配置:
grpc.MaxRecvMsgSize(64 * 1024 * 1024)、grpc.MaxSendMsgSize(64 * 1024 * 1024) - proto 中禁止定义
repeated bytes data或单个超大bytes字段 —— protoc 生成代码会尝试一次性分配整块内存 - 服务端接收时别用
for range stream.Recv(),而要用select+RecvMsg(&msg)+ 超时控制,防止单次Recv()永久挂起整个 goroutine - 分块读写是底线:客户端按 1–2MB 分块
Send(),服务端边Recv()边写磁盘,不全加载进内存
流式传输最难缠的点不在语法,而在错误路径的完整性 —— Send() 失败、Recv() 遇 EOF、上下文取消、中间件断连,每一种都要显式分支处理,少一个就变成“看起来能跑,压测必崩”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











