Back to blog
Aug 08, 2026
4 min read

Building a gRPC Service in Go

A practical introduction to building high-performance gRPC services in Go — from defining the contract to implementing the server and client.

If you work with microservices or high-performance internal APIs, chances are you have already encountered gRPC. Combined with Go, it is one of the most productive and efficient stacks for building distributed systems.

In this post we will walk through the essential steps to build a simple but complete gRPC service in Go.


1. Why gRPC?

Compared to traditional REST/JSON APIs, gRPC offers:

  • Strongly typed contracts (Protocol Buffers)
  • Efficient binary serialization
  • Built-in support for code generation
  • First-class streaming (unary, server streaming, client streaming, bidirectional)
  • Excellent performance and low latency
  • Native context and deadline propagation

These characteristics make it a great fit for internal service-to-service communication.


2. Defining the Contract (Protobuf)

Everything starts with a .proto file:

syntax = "proto3";

package user.v1;

option go_package = "github.com/leogsouza/examples/user/v1;userv1";

message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
}

message GetUserRequest {
  int64 id = 1;
}

message GetUserResponse {
  User user = 1;
}

service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

Generate the Go code:

protoc --go_out=. --go-grpc_out=. user/v1/user.proto

3. Implementing the Server

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"
	userv1 "github.com/leogsouza/examples/user/v1"
)

type server struct {
	userv1.UnimplementedUserServiceServer
}

func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
	// In a real service you would query a database here
	user := &userv1.User{
		Id:    req.Id,
		Name:  "Leonardo Souza",
		Email: "leo@example.com",
	}

	return &userv1.GetUserResponse{User: user}, nil
}

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	userv1.RegisterUserServiceServer(s, &server{})

	log.Println("gRPC server listening on :50051")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

4. Implementing a Client

conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
	log.Fatalf("did not connect: %v", err)
}
defer conn.Close()

client := userv1.NewUserServiceClient(conn)

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

resp, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 1})
if err != nil {
	log.Fatalf("GetUser failed: %v", err)
}

log.Printf("User: %v", resp.User)

5. Important Production Considerations

Timeouts and Cancellation

Always pass a context with a timeout:

ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()

Error Handling

Use gRPC status codes instead of generic errors:

import "google.golang.org/grpc/status"
import "google.golang.org/grpc/codes"

return nil, status.Errorf(codes.NotFound, "user %d not found", req.Id)

Interceptors

Interceptors are the equivalent of middleware. Use them for:

  • Logging
  • Authentication
  • Metrics
  • Tracing

Health Checks & Reflection

Enable gRPC reflection and health checking for better observability and developer experience.


6. When to Choose gRPC vs REST

Use gRPC when…Prefer REST when…
Internal service-to-service callsPublic-facing APIs
You need high performance / low latencyBrowser clients are the main consumer
You want strong contracts + streamingSimple CRUD with wide client support

Many modern systems use both: gRPC internally and REST (or gRPC-Gateway) externally.


Conclusion

gRPC + Go is a powerful combination for building efficient and reliable backend services. The strong typing, excellent performance, and first-class support for deadlines and cancellation make it particularly well suited for microservices architectures.

Key steps to remember:

  1. Define your contract with Protobuf
  2. Generate the code
  3. Implement the server (and respect context)
  4. Use proper status codes and timeouts
  5. Add interceptors for cross-cutting concerns

In future posts we can explore streaming RPCs, authentication, and more advanced production patterns.

Happy coding!