Go - Sample gRPC Streaming Service

Beginner 10/10 Teacher 10/10 Architect 10/10

Overview

This sample shows a basic gRPC server exposing a server-streaming RPC with a simple unary interceptor for logging.

Project Layout

app/
  cmd/srv/main.go
  proto/echo.proto
  internal/svc/echo.go
  go.mod

go.mod

module example.com/app

go 1.22.0

require (
  google.golang.org/grpc v1.65.0
  google.golang.org/protobuf v1.34.2
)

proto/echo.proto

syntax = "proto3";
package echo;
option go_package = "example.com/app/proto/echo;echo";

service Echo {
  rpc Stream(EchoReq) returns (stream EchoResp) {}
}

message EchoReq { string msg = 1; }
message EchoResp { string msg = 1; int32 n = 2; }

internal/svc/echo.go

package svc

import (
  "context"
  "time"
  pb "example.com/app/proto/echo"
)

type EchoSrv struct{ pb.UnimplementedEchoServer }

func (s *EchoSrv) Stream(req *pb.EchoReq, ss pb.Echo_StreamServer) error {
  for i := 0; i < 5; i++ {
    if err := ss.Send(&pb.EchoResp{Msg: req.Msg, N: int32(i)}); err != nil { return err }
    time.Sleep(200 * time.Millisecond)
  }
  return nil
}

func UnaryLog(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler)(any, error){
  start := time.Now()
  resp, err := handler(ctx, req)
  log.Printf("%s took %s", info.FullMethod, time.Since(start))
  return resp, err
}

cmd/srv/main.go

package main

import (
  "log"
  "net"
  "google.golang.org/grpc"
  pb "example.com/app/proto/echo"
  "example.com/app/internal/svc"
)

func main(){
  lis, err := net.Listen("tcp", ":50051")
  if err != nil { log.Fatal(err) }
  srv := grpc.NewServer(grpc.ChainUnaryInterceptor(svc.UnaryLog))
  pb.RegisterEchoServer(srv, &svc.EchoSrv{})
  log.Println("gRPC listening on :50051")
  log.Fatal(srv.Serve(lis))
}

Generate Stubs (example)

# requires: protoc, protoc-gen-go, protoc-gen-go-grpc in PATH
protoc -I proto --go_out=./ --go-grpc_out=./ proto/echo.proto

Test Streaming (with grpcurl)

grpcurl -plaintext -d '{"msg":"hi"}' localhost:50051 echo.Echo/Stream

Go Client Example

package main
import (
  "context"
  "log"
  "time"
  "google.golang.org/grpc"
  pb "example.com/app/proto/echo"
)
func main(){
  conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
  if err != nil { log.Fatal(err) }
  defer conn.Close()
  c := pb.NewEchoClient(conn)
  ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  defer cancel()
  stream, err := c.Stream(ctx, &pb.EchoReq{Msg:"hi"})
  if err != nil { log.Fatal(err) }
  for {
    resp, err := stream.Recv()
    if err != nil { break }
    log.Printf("%s %d", resp.GetMsg(), resp.GetN())
  }
}

Makefile (optional)

PROTO=proto/echo.proto

proto-gen:
	protoc -I proto --go_out=./ --go-grpc_out=./ $(PROTO)

run:
	go run ./cmd/srv

run-client:
	go run ./cmd/client

lint:
	go vet ./...

test:
	go test ./...