gRPC四种通信模式由.proto文件中stream关键字决定:一元(无stream)、服务端流(返回值带stream)、客户端流(参数带stream)、双向流(参数和返回值均带stream);大小写敏感,必须小写stream,且需配对定义对应message类型。

gRPC 的四种通信模式不是靠“选配置”切换的,而是由 .proto 文件里方法声明中的 stream 关键字决定的。写错一个 stream,生成的 Go 接口类型就完全不一样,客户端和服务端代码会编译不过或运行时报 UNIMPLEMENTED。
怎么在 .proto 里正确声明四种模式
.proto 文件是唯一源头,所有模式差异都体现在 service 方法签名上:
- 一元(Unary):不带
stream,参数和返回值都是单个message - 服务端流(Server Streaming):返回值带
stream,如rpc StreamLogs(LogRequest) returns (stream LogResponse); - 客户端流(Client Streaming):参数带
stream,如rpc Upload(stream Chunk) returns (UploadResult); - 双向流(Bidirectional Streaming):参数和返回值都带
stream,如rpc Chat(stream Message) returns (stream Message);
常见错误:
- 把
stream写成Stream(大小写敏感,必须小写) - 忘记为流式方法配对定义对应的
message类型(比如只写了stream Foo,但没定义message Foo) - 在 Unary 方法里误加
stream,导致生成的Server接口变成XXX_YYYServer类型,而你却按context.Context+*Req实现,编译直接报错
生成代码时漏掉 --go-grpc_out 就等于白干
protoc 命令必须同时指定 --go_out 和 --go-grpc_out,否则:
立即学习“go语言免费学习笔记(深入)”;
- 只用
--go_out:只生成.pb.go(数据结构),没有XXXClient、XXXServer接口,调用pb.NewXXXClient(conn)会报undefined - 只用
--go-grpc_out:不生成消息结构,go build会提示找不到pb.XXXRequest等类型
正确命令示例:
protoc --go_out=. --go-grpc_out=. --go-grpc_opt=paths=source_relative streaming.proto
注意:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
protoc-gen-go和protoc-gen-go-grpc插件都要go install过,且版本需匹配(2026 年推荐用@latest,避免 v1/v2 混用) - 如果
.proto在子目录,--go_opt=paths=source_relative能让生成文件路径与 proto 路径一致,不然 import 路径容易出错
服务端实现流式方法时 goroutine 和 context.Done() 是关键
流式方法的 Server 参数类型不是 context.Context,而是类似 StreamService_ServerStreamServer,它自带 Send()、Recv() 和 Context()。
必须注意:
- 服务端流(Server Streaming)不能在主线程里阻塞循环发送——要起 goroutine,否则会卡住整个 RPC 处理协程
- 客户端流(Client Streaming)必须显式循环调用
Recv(),直到返回io.EOF或 error;不检查err == io.EOF就继续读,会 panic - 所有流式方法都该监听
stream.Context().Done(),客户端断连或超时时及时退出循环,避免 goroutine 泄漏 -
Send()失败(如网络中断)应直接 return error,gRPC 框架会自动关闭流;不要忽略 err 后继续 send
示例片段(服务端流):
func (s *server) ServerStream(req *pb.Request, stream pb.StreamService_ServerStreamServer) error {
// 启动 goroutine 避免阻塞
go func() {
defer stream.Context().Done() // 不是必须,但可辅助清理
for i := 0; i < 5; i++ {
if err := stream.Send(&pb.Response{Msg: fmt.Sprintf("msg-%d", i)}); err != nil {
return // Send 失败,流已关闭
}
time.Sleep(100 * time.Millisecond)
}
}()
// 等待客户端断开或超时
<-stream.Context().Done()
return stream.Context().Err()
}客户端调用流式方法后,Recv()/Send() 必须成对、及时处理
客户端拿到的 stream 对象(如 StreamService_ClientStreamClient)不是“发完就完”,而是需要主动驱动:
- 服务端流:调用
stream.Recv()循环读,每次读一个响应;收到io.EOF表示服务端结束,不是错误 - 客户端流:先用
stream.Send()发多个请求,再调用stream.CloseAndRecv()等最终响应;不调CloseAndRecv(),服务端 recv 循环不会退出 - 双向流:可并发 go routine 分别处理
Send()和Recv(),但要注意 channel 关闭时机和context控制
容易踩的坑:
- 在 for 循环里反复
stream.Recv()却没判断err == io.EOF,导致无限循环或 panic - 客户端流忘记调
CloseAndRecv(),服务端一直等不到 EOF,goroutine 卡死 - 用
context.WithTimeout包裹流式调用,但没在Recv()/Send()中传入该 context,超时失效
真正麻烦的地方不在语法,而在流的生命周期管理:谁关 channel、谁清资源、context 怎么透传、错误怎么归因——这些在 unary 里几乎不用操心,一到流式就全冒出来。

















