Skip to main content

gRPC — Protocol Buffers and Service Definition

gRPC is a high-performance RPC framework developed by Google that uses Protocol Buffers (protobuf) as its Interface Definition Language (IDL). It provides up to 10x faster serialization speed than REST/JSON and enforces strong-typed contracts.


gRPC vs REST Comparison

ItemgRPCREST/JSON
ProtocolHTTP/2HTTP/1.1 or 2
SerializationProtocol Buffers (binary)JSON (text)
API Contract.proto file (strong type)OpenAPI (loose)
StreamingBidirectional supportLimited
BrowserNo direct supportSupported
Primary UseMicroservice communicationPublic APIs

Installation and Setup

# Install protoc compiler (https://github.com/protocolbuffers/protobuf/releases)

# Install Go plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# Install gRPC library
go get google.golang.org/grpc
go get google.golang.org/protobuf

Proto File Definition

// proto/user/v1/user.proto
syntax = "proto3";

package user.v1;

option go_package = "myapp/gen/user/v1;userv1";

import "google/protobuf/timestamp.proto";

// Message definition
message User {
int64 id = 1;
string name = 2;
string email = 3;
google.protobuf.Timestamp created_at = 4;
}

message GetUserRequest {
int64 id = 1;
}

message GetUserResponse {
User user = 1;
}

message CreateUserRequest {
string name = 1;
string email = 2;
}

message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}

message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}

// Service definition
service UserService {
// Unary RPC
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc CreateUser(CreateUserRequest) returns (User);

// Server streaming
rpc ListUsers(ListUsersRequest) returns (stream User);

// Client streaming
rpc BatchCreateUsers(stream CreateUserRequest) returns (ListUsersResponse);

// Bidirectional streaming
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
# Generate Go code
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/user/v1/user.proto

Generated files:

  • gen/user/v1/user.pb.go — Message structs
  • gen/user/v1/user_grpc.pb.go — Server/client interfaces

Server Implementation

// internal/service/user_service.go
package service

import (
"context"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"

userv1 "myapp/gen/user/v1"
)

type UserService struct {
userv1.UnimplementedUserServiceServer // Forward compatibility
repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}

// Unary RPC
func (s *UserService) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
if req.Id <= 0 {
return nil, status.Errorf(codes.InvalidArgument, "ID must be positive: %d", req.Id)
}

user, err := s.repo.FindByID(ctx, req.Id)
if err != nil {
return nil, status.Errorf(codes.Internal, "DB query failed: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "User not found: %d", req.Id)
}

return &userv1.GetUserResponse{
User: &userv1.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
CreatedAt: timestamppb.New(user.CreatedAt),
},
}, nil
}

// Server streaming RPC
func (s *UserService) ListUsers(req *userv1.ListUsersRequest, stream userv1.UserService_ListUsersServer) error {
users, err := s.repo.FindAll(stream.Context())
if err != nil {
return status.Errorf(codes.Internal, "Query failed: %v", err)
}

for _, user := range users {
if err := stream.Send(&userv1.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
}); err != nil {
return err
}
}
return nil
}

// Client streaming RPC
func (s *UserService) BatchCreateUsers(stream userv1.UserService_BatchCreateUsersServer) error {
var created []*userv1.User

for {
req, err := stream.Recv()
if err == io.EOF {
break // Client finished sending
}
if err != nil {
return status.Errorf(codes.Internal, "Stream receive failed: %v", err)
}

user, err := s.repo.Create(stream.Context(), req.Name, req.Email)
if err != nil {
return err
}
created = append(created, &userv1.User{Id: user.ID, Name: user.Name})
}

return stream.SendAndClose(&userv1.ListUsersResponse{Users: created})
}

Start gRPC Server

// cmd/server/main.go
package main

import (
"log"
"net"

"google.golang.org/grpc"
"google.golang.org/grpc/reflection"

userv1 "myapp/gen/user/v1"
"myapp/internal/service"
)

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

grpcServer := grpc.NewServer(
grpc.ChainUnaryInterceptor(
loggingInterceptor,
recoveryInterceptor,
),
)

// Register service
userv1.RegisterUserServiceServer(grpcServer, service.NewUserService(repo))

// Enable reflection for development (grpcurl service discovery)
reflection.Register(grpcServer)

log.Printf("gRPC server started: :50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Server failed: %v", err)
}
}

Client Implementation

// cmd/client/main.go
package main

import (
"context"
"log"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

userv1 "myapp/gen/user/v1"
)

func main() {
// Connect (use TLS in production)
conn, err := grpc.Dial("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil {
log.Fatalf("Connection failed: %v", err)
}
defer conn.Close()

client := userv1.NewUserServiceClient(conn)

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

// Call unary RPC
resp, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 1})
if err != nil {
// Handle gRPC error codes
if st, ok := status.FromError(err); ok {
switch st.Code() {
case codes.NotFound:
log.Printf("User not found")
case codes.DeadlineExceeded:
log.Printf("Timeout")
default:
log.Printf("Error: %v", st.Message())
}
}
return
}
log.Printf("User: %v", resp.User)

// Server streaming client
stream, err := client.ListUsers(ctx, &userv1.ListUsersRequest{})
if err != nil {
log.Fatal(err)
}
for {
user, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
log.Printf("User: %v", user)
}
}

Interceptors (Middleware)

// Server unary interceptor
func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()

resp, err := handler(ctx, req)

log.Printf("method=%s duration=%s err=%v",
info.FullMethod, time.Since(start), err)

return resp, err
}

// Panic recovery interceptor
func recoveryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
defer func() {
if r := recover(); r != nil {
err = status.Errorf(codes.Internal, "internal error: %v", r)
}
}()
return handler(ctx, req)
}

gRPC Testing

func TestGetUser(t *testing.T) {
// In-memory server
lis := bufconn.Listen(1024 * 1024)

grpcServer := grpc.NewServer()
userv1.RegisterUserServiceServer(grpcServer, mockService)
go grpcServer.Serve(lis)
defer grpcServer.Stop()

// Client connection
conn, _ := grpc.DialContext(ctx, "bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
defer conn.Close()

client := userv1.NewUserServiceClient(conn)
resp, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 1})

assert.NoError(t, err)
assert.Equal(t, int64(1), resp.User.Id)
}

Key Takeaways

RPC TypeDefinitionUse Case
Unaryrpc Method(Req) returns (Resp)Standard request/response
Server streamingreturns (stream Resp)Large data transfer
Client streaming(stream Req) returns (Resp)Upload
Bidirectional streaming(stream Req) returns (stream Resp)Chat, real-time
  • Embed UnimplementedXxxServer to prevent compilation errors when adding new methods
  • Use status.Errorf(codes.NotFound, "...") for standard gRPC error returns
  • Interceptors serve the same role as HTTP middleware — logging/auth/recovery