#golang#grpc

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."

By huud
0 views
~3 min read
Build and Test gRPC in Go

Prerequisites

Install compiler and Go plugins:

bash
# 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:

protobuf
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:

bash
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:

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:

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.

bash
go run server/main.go

Terminal 2: Run client.

bash
go run client/main.go

Output:

text
Hello Gopher

Add Context Timeout

RPC without deadline risks hanging. Set timeout on client call.

Update client/main.go:

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:

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:

bash
go test -v ./server

skipped: mock generators, external assertion libraries.
add when: test cases require mocking deep database dependencies.

About the Author

huud

huud

@huud

About →

Systems architect and software engineer building high-performance distributed platforms.