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
| Item | gRPC | REST/JSON |
|---|---|---|
| Protocol | HTTP/2 | HTTP/1.1 or 2 |
| Serialization | Protocol Buffers (binary) | JSON (text) |
| API Contract | .proto file (strong type) | OpenAPI (loose) |
| Streaming | Bidirectional support | Limited |
| Browser | No direct support | Supported |
| Primary Use | Microservice communication | Public 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 structsgen/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 Type | Definition | Use Case |
|---|---|---|
| Unary | rpc Method(Req) returns (Resp) | Standard request/response |
| Server streaming | returns (stream Resp) | Large data transfer |
| Client streaming | (stream Req) returns (Resp) | Upload |
| Bidirectional streaming | (stream Req) returns (stream Resp) | Chat, real-time |
- Embed
UnimplementedXxxServerto 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