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
<p>常见坑:<code>SendAndClose()</code> 只存在于 Unary 和 Client Streaming 的服务端 handler 中;在这里调用会编译报错。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig"><img
src="https://img.php.cn/upload/skill/000/000/081/179215391842073.jpg" alt="Golang Uber Dig" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig" class="overflowclass">Golang Uber Dig</a>
<p class="overflowclass">使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等</p>
</div>
<a rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
<h3>Client Streaming:必须显式调用 <code>CloseSend()</code>
</h3>
<p>客户端流中,客户端拿到的是 <code>ClientStream</code>(如 <code>HelloService_SayHelloClient</code>),它有 <code>Send()</code> 和 <code>CloseSend()</code>。不调 <code>CloseSend()</code>,服务端永远收不到“发送完毕”信号,会一直阻塞在 <code>Recv()</code> 或超时。</p>
- 所有
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 超时来收尾。










