正在加载,请稍候…

Building Go Microservices with gRPC: Streaming, Interceptors, and Load Balancing

Build production Go microservices with gRPC: define protobuf services, implement unary and streaming RPCs, write authentication and logging interceptors, handle errors with status codes, and configure load balancing.

Building Go Microservices with gRPC: Streaming, Interceptors, and Load Balancing

gRPC has become the standard inter-service communication protocol for Go microservices, offering strong typing via Protocol Buffers, bidirectional streaming, and excellent performance. This guide covers everything from defining your first protobuf service to production load balancing.

Why gRPC for Go Microservices

gRPC advantages over REST:

  • Strong typing: Protocol Buffers enforce schemas at compile time
  • Performance: Binary serialization vs. JSON text
  • Streaming: Native support for server, client, and bidirectional streaming
  • Code generation: Clients and servers generated from .proto files
  • Interceptors: Middleware analogous to HTTP middleware

Defining Protobuf Services

// user.proto
syntax = "proto3";
package user.v1;
option go_package = "github.com/example/app/gen/user/v1;userv1";

import "google/protobuf/timestamp.proto";

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

message GetUserRequest {
  string id = 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;
}

// Streaming: watch for user updates
message WatchUsersRequest {
  repeated string ids = 1;
}

message UserEvent {
  enum Type {
    CREATED = 0;
    UPDATED = 1;
    DELETED = 2;
  }
  Type type = 1;
  User user = 2;
}

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
  
  // Server streaming: server sends multiple responses
  rpc WatchUsers(WatchUsersRequest) returns (stream UserEvent);
  
  // Client streaming: client sends multiple requests
  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 \
       user.proto

Implementing the Server

package main

import (
    "context"
    "net"
    "log"
    
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    
    userv1 "github.com/example/app/gen/user/v1"
)

type UserServer struct {
    userv1.UnimplementedUserServiceServer
    store UserStore
}

func (s *UserServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
    user, err := s.store.Get(ctx, req.Id)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return nil, status.Errorf(codes.NotFound, "user %s not found", req.Id)
        }
        return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
    }
    return userToProto(user), nil
}

func (s *UserServer) WatchUsers(req *userv1.WatchUsersRequest, stream userv1.UserService_WatchUsersServer) error {
    ctx := stream.Context()
    eventCh := s.store.Subscribe(req.Ids)
    defer s.store.Unsubscribe(eventCh)
    
    for {
        select {
        case event, ok := <-eventCh:
            if !ok {
                return nil
            }
            if err := stream.Send(&userv1.UserEvent{
                Type: userv1.UserEvent_UPDATED,
                User: userToProto(event.User),
            }); err != nil {
                return err
            }
        case <-ctx.Done():
            return ctx.Err()
        }
    }
}

func (s *UserServer) BatchCreateUsers(stream userv1.UserService_BatchCreateUsersServer) error {
    var created []*userv1.User
    for {
        req, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            return status.Errorf(codes.Internal, "failed to receive: %v", err)
        }
        user, err := s.store.Create(stream.Context(), req.Name, req.Email)
        if err != nil {
            return status.Errorf(codes.Internal, "failed to create user: %v", err)
        }
        created = append(created, userToProto(user))
    }
    return stream.SendAndClose(&userv1.ListUsersResponse{Users: created})
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    
    server := grpc.NewServer(
        grpc.ChainUnaryInterceptor(
            loggingInterceptor,
            authInterceptor,
            recoveryInterceptor,
        ),
        grpc.ChainStreamInterceptor(
            streamLoggingInterceptor,
        ),
    )
    
    userv1.RegisterUserServiceServer(server, &UserServer{})
    
    log.Println("gRPC server listening on :50051")
    if err := server.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

gRPC Interceptors

Interceptors provide middleware functionality for gRPC:

// Unary interceptor for logging
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("%s completed in %v, err: %v", info.FullMethod, time.Since(start), err)
    return resp, err
}

// Authentication interceptor
func authInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "missing metadata")
    }
    
    tokens := md.Get("authorization")
    if len(tokens) == 0 {
        return nil, status.Error(codes.Unauthenticated, "missing authorization token")
    }
    
    if !validateToken(tokens[0]) {
        return nil, status.Error(codes.Unauthenticated, "invalid token")
    }
    
    return handler(ctx, req)
}

// Recovery interceptor for panics
func recoveryInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (resp interface{}, err error) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("panic in %s: %v", info.FullMethod, r)
            err = status.Errorf(codes.Internal, "internal server error")
        }
    }()
    return handler(ctx, req)
}

Error Handling with Status Codes

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "google.golang.org/genproto/googleapis/rpc/errdetails"
)

func (s *UserServer) CreateUser(ctx context.Context, req *userv1.CreateUserRequest) (*userv1.User, error) {
    // Validation with detailed error info
    if req.Email == "" {
        st, _ := status.New(codes.InvalidArgument, "email is required").
            WithDetails(&errdetails.BadRequest{
                FieldViolations: []*errdetails.BadRequest_FieldViolation{
                    {Field: "email", Description: "email cannot be empty"},
                },
            })
        return nil, st.Err()
    }
    
    user, err := s.store.Create(ctx, req.Name, req.Email)
    if err != nil {
        if errors.Is(err, ErrEmailConflict) {
            return nil, status.Errorf(codes.AlreadyExists, "email %s already exists", req.Email)
        }
        return nil, status.Errorf(codes.Internal, "create user: %v", err)
    }
    return userToProto(user), nil
}

Retry Logic with Exponential Backoff

import "google.golang.org/grpc/backoff"

func newClientWithRetry(addr string) (*grpc.ClientConn, error) {
    retryPolicy := `{
        "methodConfig": [{
            "name": [{"service": "user.v1.UserService"}],
            "retryPolicy": {
                "maxAttempts": 4,
                "initialBackoff": "0.1s",
                "maxBackoff": "1s",
                "backoffMultiplier": 2,
                "retryableStatusCodes": ["UNAVAILABLE", "RESOURCE_EXHAUSTED"]
            }
        }]
    }`
    
    return grpc.Dial(addr,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithDefaultServiceConfig(retryPolicy),
    )
}

Load Balancing

// Round-robin load balancing across multiple backends
func newLoadBalancedClient(endpoints []string) (*grpc.ClientConn, error) {
    // Use DNS-based service discovery
    return grpc.Dial(
        "dns:///user-service.default.svc.cluster.local:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
    )
}

// Manual resolver for custom service discovery
type StaticResolver struct {
    endpoints []string
}

func (r *StaticResolver) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
    addrs := make([]resolver.Address, len(r.endpoints))
    for i, ep := range r.endpoints {
        addrs[i] = resolver.Address{Addr: ep}
    }
    cc.UpdateState(resolver.State{Addresses: addrs})
    return r, nil
}

Health Checking

import "google.golang.org/grpc/health/grpc_health_v1"

type HealthServer struct{}

func (h *HealthServer) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {
    // Check database, dependencies
    if err := checkDependencies(ctx); err != nil {
        return &grpc_health_v1.HealthCheckResponse{
            Status: grpc_health_v1.HealthCheckResponse_NOT_SERVING,
        }, nil
    }
    return &grpc_health_v1.HealthCheckResponse{
        Status: grpc_health_v1.HealthCheckResponse_SERVING,
    }, nil
}

func (h *HealthServer) Watch(req *grpc_health_v1.HealthCheckRequest, stream grpc_health_v1.Health_WatchServer) error {
    // Stream health updates
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            stream.Send(&grpc_health_v1.HealthCheckResponse{
                Status: grpc_health_v1.HealthCheckResponse_SERVING,
            })
        case <-stream.Context().Done():
            return nil
        }
    }
}

Conclusion

gRPC with Go provides a powerful foundation for microservice communication. Protocol Buffers give you type safety and efficient serialization. Interceptors handle cross-cutting concerns like auth and logging. Built-in streaming eliminates the need for WebSockets in most cases. Combined with proper error codes, retry logic, and health checking, you have everything needed to build production-grade microservice infrastructure.