Build and Test gRPC in Go
"Build Go gRPC service from Protobuf schema. Generate stubs, implement server and client with context timeout, test in-memory using bufconn."
Prerequisites
Install compiler and Go plugins:
# Install protoc via OS package manager (brew install protobuf / apt install -y protobuf-compiler)
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH="$PATH:$(go env GOPATH)/bin"
Define Schema
proto/greet.proto:
syntax = "proto3";
package greet;
option go_package = "./greet";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
Generate Go stubs:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/greet.proto
Server Implementation
server/main.go:
package main
import (
"context"
"log"
"net"
pb "example.com/project/proto"
"google.golang.org/grpc"
)
type server struct {
pb.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + req.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// ponytail: basic plaintext server. Add TLS via credentials.NewServerTLSFromFile when prod.
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Client Implementation
client/main.go:
package main
import (
"context"
"fmt"
"log"
pb "example.com/project/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
res, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "Gopher"})
if err != nil {
log.Fatalf("call failed: %v", err)
}
fmt.Println(res.GetMessage())
}
skipped: TLS credentials, interceptors, streaming.
add when: prod deployment requires secure transit or auth middleware.
Run Application
Terminal 1: Start server.
go run server/main.go
Terminal 2: Run client.
go run client/main.go
Output:
Hello Gopher
Add Context Timeout
RPC without deadline risks hanging. Set timeout on client call.
Update client/main.go:
package main
import (
"context"
"fmt"
"log"
"time"
pb "example.com/project/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
// ponytail: hardcoded 1s timeout. Use config or trace context propagation for dynamic deadlines.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := client.SayHello(ctx, &pb.HelloRequest{Name: "Gopher"})
if err != nil {
log.Fatalf("call failed: %v", err)
}
fmt.Println(res.GetMessage())
}
skipped: dynamic timeout injection, OpenTelemetry tracing.
add when: distributed service meshes require end-to-end deadline propagation.
Integration Test
Test server logic in-memory using google.golang.org/grpc/test/bufconn. No open network port needed.
server/main_test.go:
package main
import (
"context"
"net"
"testing"
pb "example.com/project/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
)
const bufSize = 1024 * 1024
func TestSayHello(t *testing.T) {
lis := bufconn.Listen(bufSize)
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
go func() {
if err := s.Serve(lis); err != nil && err != grpc.ErrServerStopped {
t.Errorf("server error: %v", err)
}
}()
defer s.Stop()
conn, err := grpc.NewClient("passthrough://bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatalf("dial failed: %v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
res, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "TestUser"})
if err != nil {
t.Fatalf("SayHello failed: %v", err)
}
want := "Hello TestUser"
if res.GetMessage() != want {
t.Fatalf("got %q, want %q", res.GetMessage(), want)
}
}
Run test:
go test -v ./server
skipped: mock generators, external assertion libraries.
add when: test cases require mocking deep database dependencies.
About the Author
huud
@huud
Systems architect and software engineer building high-performance distributed platforms.