gRPC双向流函数名须以stream开头且返回stream类型,方法签名必须严格匹配protobuf定义,使用生成的XXXServer类型并正确处理io.EOF和Send失败,客户端需传入可取消context以防goroutine泄漏。

gRPC双向流函数名必须是 stream 开头且返回 stream 类型
Go 的 gRPC 服务端方法签名不是随便写的,必须严格匹配 protobuf 定义的 rpc 声明。如果你在 .proto 文件里写的是 rpc Chat(stream Message) returns (stream Message);,那生成的 Go 方法签名就只能是:func (s *server) Chat(stream pb.ChatServer) error —— 注意参数类型是 pb.ChatServer(不是 *grpc.ServerStream),它自带 Recv() 和 Send() 方法。
常见错误现象:自己手写一个带两个 channel 的函数、或试图用 context.WithTimeout 包裹 stream 参数,结果编译不报错但客户端永远收不到响应,因为 gRPC 运行时根本没调你的函数。
- 别手动实现流接口,只用 protobuf 生成的
XXXServer类型 -
Recv()返回io.EOF表示客户端关闭了发送端,不是错误,要主动退出循环 -
Send()失败通常意味着客户端断连,此时应直接 return 错误,让 gRPC 自动清理连接
客户端发起双向流时不能复用 context.Background()
双向流生命周期长,客户端必须传入可取消的 context,否则一旦流卡住(比如服务端 hang 住、网络抖动),整个 goroutine 就永久泄漏。用 context.Background() 看似省事,实际等于放弃控制权。
使用场景:实时聊天、协同编辑、设备状态心跳推送——这些都不是“发一次收一次”能解决的,需要明确超时和中断机制。
立即学习“go语言免费学习笔记(深入)”;
- 用
context.WithTimeout(context.Background(), 30*time.Second)设硬性上限 - 若需用户手动中断(如点击“结束会话”按钮),用
context.WithCancel,并在 UI 事件中调用 cancel 函数 - 不要在
stream.Send()后立刻stream.CloseSend(),除非你确定不再发数据;否则服务端Recv()会提前收到io.EOF
流中频繁 Send/Recv 会导致 rpc error: code = Unavailable desc = transport is closing
这不是你的代码写错了,而是底层 HTTP/2 连接被对端静默关闭,常见于服务端处理慢、客户端发太快、或 NAT 超时。gRPC 默认不重试流方法,出错即终止。
性能影响:每次重连都要走 TLS 握手 + HTTP/2 settings 交换,延迟明显;如果业务逻辑没做幂等,重复消息还会导致状态错乱。
- 服务端务必给每个
Recv()加select { case ,避免阻塞在读取上 - 客户端发消息前检查
stream.Context().Err() == nil,避免往已失效的 stream 写数据 - 不要依赖单次流承载所有业务;对长周期通信,建议用 stream 承载“指令帧”,具体数据走独立上传接口
Go 的 grpc.ClientConn 必须手动 Close,且不能跨 goroutine 复用
很多人以为 grpc.Dial() 返回的 conn 是线程安全的,其实不然:ClientConn 内部有连接池和 resolver,但它的方法(比如 NewStream)不是并发安全的。更关键的是,不 Close() 会导致 fd 泄漏、DNS 缓存不更新、TLS session 无法复用。
容易踩的坑:把 ClientConn 当全局变量,在多个 handler 里反复调 client.Chat(ctx),最后发现连接数疯涨、netstat -an | grep :443 | wc -l 持续上涨。
- 每个逻辑单元(比如一次会话)建议新建并管理自己的
ClientConn,用完立即conn.Close() - 若必须复用,确保只在一个 goroutine 中创建 stream,其他 goroutine 通过 channel 投递消息
- 检查
grpc.WithBlock()是否开启——它会让Dial()阻塞到连接建立,测试环境看着正常,生产遇到 DNS 慢就会卡死










