gRPC双向流需在.proto中请求和响应类型前均加stream关键字,否则生成单向流接口导致类型不匹配;服务端方法签名不可手动添加context.Context;客户端须用两个goroutine分别处理Send/Recv并监听stream.Context()。

proto 文件里必须同时写两个 stream 关键字
双向流不是“加个 flag”或者配个选项,而是由 .proto 语法硬性规定的:请求类型和响应类型前都得有 stream。写错一个就变成单向流,生成的 Go 接口会完全不对。
-
rpc Chat(stream Message) returns (stream Message)✅ 双向流 -
rpc Chat(Message) returns (stream Message)❌ 服务端流(客户端只能发一次) -
rpc Chat(stream Message) returns (Message)❌ 客户端流(服务端只能回一次)
漏掉任意一个 stream,protoc 生成的客户端类型是 *ChatClient,服务端接收参数却是 ChatServer 或 ChatClientStream——类型不匹配,编译能过,但运行时根本不会被 gRPC 调用,客户端永远卡在等待响应。
服务端函数签名不能手动加 context.Context
生成的服务端方法签名是固定的,例如 func (s *server) Chat(stream pb.ChatService_ChatServer) error。你不能改成 func (s *server) Chat(ctx context.Context, stream pb.ChatService_ChatServer) error,也不能在函数体内直接用传入的 ctx 去控制流生命周期。
- 正确做法:从
stream.Context()拿上下文,它已由 gRPC 自动绑定连接生命周期 - 错误现象:自己塞一个
context.WithTimeout进去,会导致超时后stream.Recv()突然返回context.Canceled,但服务端逻辑还没清理资源,连接可能卡在半关闭状态 - Recv() 返回
io.EOF表示客户端调用了CloseSend(),不是错误,要主动退出循环;返回其他 err(如status.Error或网络断开)才该 return 错误让 gRPC 清理
客户端必须用两个 goroutine 分别处理收发
双向流不是请求-响应模型,Send() 和 Recv() 是完全异步的。在同一个 goroutine 里交替调用,极易因某次 Recv() 阻塞而卡死整个流,导致后续 Send() 永远发不出去。
立即学习“go语言免费学习笔记(深入)”;
- 发数据的 goroutine 必须监听
stream.Context().Done(),一旦上下文取消,立刻停止Send(),避免 panic - 收数据的 goroutine 要持续
for { res, err := stream.Recv(); ... },不能只调一次 - 不要在
Send()后立刻CloseSend(),除非你确定客户端不会再发任何消息;否则服务端Recv()会提前收到io.EOF,中断接收逻辑 - 客户端初始化流时,绝不能用
context.Background(),必须用context.WithCancel或context.WithTimeout,否则流 hang 住就会永久泄漏 goroutine
Send() 失败别重试,Recv() 遇到 io.EOF 别当错误处理
gRPC 双向流底层基于 HTTP/2,Send() 失败大概率意味着连接已断或对端不可达。此时重试只会让问题更模糊;而 io.EOF 在 Recv() 中出现,只表示对方调用了 CloseSend(),属于正常结束信号。
-
Send()返回非 nil error → 直接 return,让 gRPC 关闭连接并触发重连逻辑(如果业务层有) -
Recv()返回io.EOF→ break 循环,干净退出 goroutine -
Recv()返回status.Code == codes.Canceled或codes.Unavailable→ 按需判断是否重连,但注意幂等性,避免重复消息 - 频繁
Send()/Recv()触发rpc error: code = Unavailable desc = transport is closing,通常是 NAT 超时或服务端处理慢,不是代码 bug,应检查网络和服务器负载,而非改逻辑
最常被忽略的是:每个消息实例必须新建,别复用同一个 Message{} 对象反复赋值再 Send()——proto 序列化会复用内部 buffer,客户端收到的可能是上一轮旧数据。


















