gRPC流类型由proto中stream关键字位置决定:server streaming用Send()多次推送,client streaming需SendAndClose(),bidirectional streaming双方独立读写;stream必须紧邻类型声明。

Go 的 gRPC 流式传输不是“开个开关”就能用的,核心在于 proto 定义里 stream 关键字的位置——它直接决定你拿到的是哪种流、用什么 API、连错误处理逻辑都不同。
proto 文件里 stream 出现在哪,就决定了流类型
gRPC 流式行为完全由 .proto 中 rpc 方法签名里的 stream 修饰符位置决定,Go 代码生成器会据此生成完全不同的客户端和服务端接口。别指望改 Go 代码绕过这个约束。
-
rpc SayHello(stream HelloRequest) returns (HelloResponse);→ 服务端流(Server Streaming):客户端发一次,服务端回多次 -
rpc SayHello(HelloRequest) returns (stream HelloResponse);→ 客户端流(Client Streaming):客户端发多次,服务端收完才回一次 -
rpc SayHello(stream HelloRequest) returns (stream HelloResponse);→ 双向流(Bidirectional Streaming):双方各自维持独立的读写流
注意:stream 修饰的是消息类型,不是字段名;必须紧挨着类型写,空格或换行都不行。写错会导致 protoc 报 Expected "stream" or "returns" 错误。
Server Streaming:用 Send() 而不是 SendAndClose()
服务端流的 handler 接收一个 ServerStream(如 HelloService_SayHelloServer),它只有 Send() 方法,没有 SendAndClose()。调用 Send() 多次即可持续推送,结束时函数自然返回——别手动 close 或 return error 来“终止流”,那会中断连接。
func (s *server) SayHello(req *pb.HelloRequest, stream pb.HelloService_SayHelloServer) error {
for i := 0; i < 5; i++ {
if err := stream.Send(&pb.HelloResponse{Message: fmt.Sprintf("Hello %d", i)}); err != nil {
return err // 网络断开等真实错误才 return
}
time.Sleep(time.Second)
}
return nil // 正常结束,流自动关闭
}
常见坑:SendAndClose() 只存在于 Unary 和 Client Streaming 的服务端 handler 中;在这里调用会编译报错。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
Client Streaming:必须显式调用 CloseSend()
客户端流中,客户端拿到的是 ClientStream(如 HelloService_SayHelloClient),它有 Send() 和 CloseSend()。不调 CloseSend(),服务端永远收不到“发送完毕”信号,会一直阻塞在 Recv() 或超时。
- 所有
Send()必须在CloseSend()前完成 -
CloseSend()后不能再调Send(),否则 panic:send on closed stream - 服务端收到
CloseSend()后才会执行后续逻辑(比如聚合、计算、返回响应)
stream, _ := client.SayHello(context.Background())
for _, name := range []string{"a", "b", "c"} {
stream.Send(&pb.HelloRequest{Name: name})
}
stream.CloseSend() // 关键!缺了这句,服务端卡住
resp, _ := stream.Recv() // 这里才真正拿到响应
Bidirectional Streaming:读写要分开 goroutine,且注意 Recv() 阻塞
双向流的客户端和服务端都要同时读和写,但 Recv() 是阻塞的——如果只在一个 goroutine 里顺序调 Send() 再 Recv(),第二次 Recv() 就会卡死,因为对方还没发过来。
正确做法是启动两个 goroutine:一个专发,一个专收。用 sync.WaitGroup 或 context.WithCancel 控制生命周期。
- 服务端:
Recv()循环里处理请求,Send()异步推响应,别混在一起 - 客户端:同样分离读写;若需按序响应,自己加锁或 channel 缓存
- 任意一方
Recv()返回io.EOF或 error,说明对方已关闭流,应停止读并尽快退出
最容易被忽略的是:双向流没有“默认结束机制”。哪怕双方都发完了,只要没人关连接,TCP 连接就挂着。必须靠业务层约定(比如发 type: EOF 消息)或 context 超时来收尾。

















