Description
Use case(s)
Suppose you have the following service definitions:
message HelloRequest {
string name = 1;
}
message HelloReply {
string greeting = 1;
}
service Greeter {
rpc StoreHello (HelloRequest) returns (google.protobuf.Empty) {}
rpc SayHello (google.protobuf.Empty) returns (stream HelloReply) {}
}
service GreeterReadOnly {
rpc SayHello (google.protobuf.Empty) returns (stream HelloReply) {}
}
The idea here being that someone with access to a Greeter client could call StoreHello multiple times to store many names that should be greeted, and then either a Greeter client or a GreeterReadOnly client can call SayHello to get a stream of all the stored replies. Obviously this is a toy example, but you can see a real example of this same pattern here. The salient feature here is this: we have two different services which define the exact same streaming RPC.
Now because both Greeter and GreeterReadOnly share functionality, we want to use a single implementation type to implement both of them. So we write something like this:
type GreeterImpl struct {
names []string
}
func (g *GreeterImpl) StoreHello(ctx context.Context, req *pb.HelloRequest) (*emptypb.Empty, error) {
...
}
func (g *GreeterImpl) SayHello(req *emptypb.Empty, stream pb.Greeter_SayHelloServer) error {
...
}
But of course when we go to Register this implementation, it doesn't actually work. It satisfies the Greeter
interface, but not the GreeterReadOnly
interface.
func main() {
grpcServer := grpc.NewServer()
pb.RegisterGreeterServer(grpcServer, &GreeterImpl{}) // This succeeds
pb.RegisterGreeterReadOnlyServer(grpcServer, &GreeterImpl{}) // This fails at compile time
}
This is because grpc-go has generated the following interfaces for us:
type GreeterServer interface {
StoreHello(*HelloRequest) (*emptypb.Empty, error)
SayHello(*HelloRequest, Greeter_SayHelloServer) error
}
type GreeterReadOnlyServer interface {
SayHello(*HelloRequest, GreeterReadOnly_SayHelloServer) error
}
The SayHello
methods in these two interfaces do not have the same signature, despite having the exact same definition in the original .proto file. This means that they can never both be implemented by the same Go struct.
Note: this is different from the case for Unary RPCs: if two different gRPC services define the same unary rpc, then the generated Go methods have the exact same signature and can both be implemented by the same type.
Proposed Solution
The obvious solution here is that the stream object could be named by the message type(s) it is capable of streaming, rather than by the service and rpc which define it. For example, instead of Greeter_SayHelloServer
, the type could be named HelloReplyServerStreamServer
, indicating that it a) is the Server half of the stream object, b) that it is for a Server-Streaming (i.e. one request, many replies) method, and c) that it streams HelloReply
messages. Then this same exact type could be used in both versions of the SayHello
method, giving them the same signature, and allowing them to be implemented by a single type.
But I propose going even further: rather than generating a unique type for every stream, I think that protoc-gen-go-grpc should instead take advantage of Go's generics support and define only six streaming types, parameterized by the message type they stream:
// ServerStreamClient represents the client side of a server-streaming (one request, many responses) RPC.
type ServerStreamClient[T] interface {
Recv() (*T, error)
grpc.ClientStream
}
type serverStreamClient struct {
grpc.ClientStream
}
func (x *serverStreamClient[T]) Recv() (*T, error) {
m := new(T)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// ServerStreamServer represents the server side of a server-streaming (one request, many responses) RPC.
type ServerStreamServer[T] interface {
Send(*T) error
grpc.ServerStream
}
type serverStreamServer[T] struct {
grpc.ServerStream
}
func (x *serverStreamServer[T]) Send(m *T) error {
return x.ServerStream.SendMsg(m)
}
// ClientStreamClient represents the client side of a client-streaming (many requests, one response) RPC.
type ClientStreamClient[T] interface { ... }
// ClientStreamServer represents the server side of a client-streaming (many requests, one response) RPC.
type ClientStreamServer[T] interface { ... }
// BidiStreamClient represents the client side of a bidirectional-streaming (many requests, many responses) RPC.
type BidiStreamClient[T, U] interface { ... }
// BidiStreamServer represents the server side of a bidirectional-streaming (many requests, many responses) RPC.
type BidiStreamServer[T, U] interface { ... }
Then the generated code for the service interfaces and their SayHello
method would simply be reduced to:
type GreeterClient interface {
StoreHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
SayHello(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (ServerStreamClient[HelloReply], error)
}
type GreeterReadOnlyClient interface {
SayHello(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (ServerStreamClient[HelloReply], error)
}
func (c *greeterClient) SayHello(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (ServerStreamClient[HelloReply], error) {
stream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[2], "/pb.Greeter/SayHello", opts...)
if err != nil {
return nil, err
}
x := &serverStreamClient[HelloReply]{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type GreeterServer interface {
StoreHello(*HelloRequest) (*emptypb.Empty, error)
SayHello(*emptypb.Empty, ServerClientServer[HelloReply]) error
}
type GreeterReadOnlyServer interface {
SayHello(*emptypb.Empty, ServerClientServer[HelloReply]) error
}
func _Greeter_SayHello_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(HelloReply)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(GreeterServer).SayHello(m, &serverStreamServer[HelloReply]{stream})
}
Note first that this eliminates the need for there ever to be service- or method-specific types in the protoc-gen-go-grpc generated code. And in fact these [Server|Client|Bidi]Stream[Server|Client]
generic types could be defined once in the gRPC package itself and then simply referenced in the generated code.
But note more importantly that this means that both GreeterServer.SayHello
and GreeterReadOnlyServer.SayHello
have the exact same function signature, meaning that they can both be implemented by a single type:
type GreeterImpl struct {
names []string
}
func (g *GreeterImpl) StoreHello(ctx context.Context, req *pb.HelloRequest) (*emptypb.Empty, error) {
...
}
func (g *GreeterImpl) SayHello(req *emptypb.Empty, stream grpc.ServerStreamServer[pb.HelloReply]) error {
...
}
Additional Context
This proposal is obviously not backwards-compatible, and if it were adopted it would mean that the new version of protoc-gen-go-grpc cannot be used by versions of Go prior to when it got generics (1.18). But I think it is sufficiently elegant that it is worth considering anyway.