Tutorial gRPC Go: Construye microservicios gRPC en Go, paso a paso
Este tutorial gRPC Go guía a desarrolladores backend de nivel intermedio para construir microservicios gRPC robustos con Go, desde cero y con prácticas de producción. Cubriremos Protocol Buffers Go, seguridad con TLS/mTLS gRPC Go, exposición REST con gRPC Gateway REST, observabilidad, pruebas, contenedorización con Docker gRPC y más.
1) Requisitos previos e instalación
- Go 1.22+ (obligatorio)
- protoc (Protocol Buffers compiler)
- Plugins: protoc-gen-go, protoc-gen-go-grpc
- Herramientas: grpcurl, grpcui
- Docker y Docker Compose
- Postman o Insomnia (opcional)
1.1 Instalación rápida
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Go 1.22+ go version # protoc (>= 3.21 recomendado) protoc --version # Plugins Go (usar Go install) go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest # gRPC-Gateway (para REST/OpenAPI) go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest # grpcurl y grpcui brew install grpcurl grpcui # macOS (o usar releases en GitHub) |
Explicación: verificas Go y protoc; instalas los plugins necesarios para generar stubs en Go, y las utilidades para ejecutar y depurar llamadas gRPC.
Checklist
- Go 1.22+ instalado y en PATH
- protoc accesible y versión verificada
- protoc-gen-go y protoc-gen-go-grpc instalados
- grpcurl y grpcui instalados
- Docker y Docker Compose funcionando
2) Estructura de proyecto y configuración
2.1 Inicializa el módulo y estructura recomendada
1 2 3 4 |
mkdir grpc-go-microservice && cd $_ go mod init github.com/tuorg/grpc-go-microservice mkdir -p cmd/server cmd/client internal/{server,client,config,proto,service,repo,db,auth,interceptors,observability} pkg migrations |
Explicación: creas el módulo Go y una estructura clara para separar responsabilidades (servidor, cliente, lógica de negocio, repositorios, configuración, interceptores y observabilidad).
2.2 Variables de entorno y configuración
Archivo .env (desarrollo):
1 2 3 4 5 6 7 8 9 10 11 |
APP_ENV=dev GRPC_ADDR=:50051 HTTP_ADDR=:8080 METRICS_ADDR=:2112 DB_DSN=postgres://user:pass@db:5432/app?sslmode=disable JWT_SECRET=supersecret TLS_CERT_FILE=certs/server.crt TLS_KEY_FILE=certs/server.key CA_CERT_FILE=certs/ca.crt OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 |
Explicación: definimos endpoints, DSN de base de datos, secretos y parámetros de observabilidad para cada entorno.
Archivo internal/config/config.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package config import ( "log" "github.com/kelseyhightower/envconfig" ) type Config struct { AppEnv string `envconfig:"APP_ENV" default:"dev"` GRPCAddr string `envconfig:"GRPC_ADDR" default:":50051"` HTTPAddr string `envconfig:"HTTP_ADDR" default:":8080"` MetricsAddr string `envconfig:"METRICS_ADDR" default:":2112"` DBDSN string `envconfig:"DB_DSN"` JWTSecret string `envconfig:"JWT_SECRET"` TLSCert string `envconfig:"TLS_CERT_FILE"` TLSKey string `envconfig:"TLS_KEY_FILE"` CACert string `envconfig:"CA_CERT_FILE"` OTLPEndpoint string `envconfig:"OTEL_EXPORTER_OTLP_ENDPOINT"` } func Load() Config { var c Config if err := envconfig.Process("", &c); err != nil { log.Fatalf("error loading config: %v", err) } return c } |
Explicación: usamos envconfig para mapear variables de entorno a una estructura tipada, con valores por defecto razonables.
Checklist
- go mod init ejecutado
- Árbol de carpetas creado
- .env preparado por entorno
- Config loader implementado
3) Definición del contrato (.proto)
Archivo internal/proto/user/v1/user.proto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
syntax = "proto3"; package user.v1; option go_package = "github.com/tuorg/grpc-go-microservice/internal/proto/user/v1;userv1"; import "google/api/annotations.proto"; import "google/protobuf/timestamp.proto"; import "google/rpc/status.proto"; // Servicio de usuarios: ejemplo de microservicios gRPC service UserService { // RPC Unary rpc CreateUser(CreateUserRequest) returns (User) { option (google.api.http) = { post: "/v1/users" body: "*" }; } // Server Streaming rpc WatchUsers(WatchUsersRequest) returns (stream User) { option (google.api.http) = { get: "/v1/users:watch" }; } // Client Streaming rpc UploadAvatars(stream AvatarChunk) returns (UploadSummary) { option (google.api.http) = { post: "/v1/users/avatars:upload" body: "*" }; } // Bidirectional Streaming rpc Chat(stream ChatMessage) returns (stream ChatMessage) {} } message CreateUserRequest { string email = 1; // usar kebab-case en HTTP, snake_case en protobuf string name = 2; } message User { string id = 1; string email = 2; string name = 3; google.protobuf.Timestamp created_at = 4; } message WatchUsersRequest { int32 limit = 1; } message AvatarChunk { bytes content = 1; string filename = 2; } message UploadSummary { int32 count = 1; } message ChatMessage { string from = 1; string text = 2; } // Convenciones de errores (google.rpc.Status) message ErrorDetail { string field = 1; string description = 2; } |
Explicación: definimos RPCs de cuatro tipos (unary, server, client y bidi streaming), añadimos annotations para gRPC Gateway, y dejamos espacio para detalles de error estructurados.
Checklist
- Mensajes y servicios definidos
- Annotations HTTP incluidas
- Convenciones de nombres documentadas
4) Generación de código
Comandos para generar stubs en Go, gateway y OpenAPI:
1 2 3 4 5 6 7 8 9 10 11 |
PROTO_DIR=internal/proto OUT_DIR=. protoc -I $PROTO_DIR -I third_party/googleapis --go_out=$OUT_DIR --go_opt=paths=source_relative --go-grpc_out=$OUT_DIR --go-grpc_opt=paths=source_relative --grpc-gateway_out $OUT_DIR --grpc-gateway_opt logtostderr=true,paths=source_relative --openapiv2_out . --openapiv2_opt logtostderr=true $PROTO_DIR/user/v1/user.proto |
Explicación: generamos los archivos .pb.go y _grpc.pb.go, el reverse-proxy para REST y el documento OpenAPI. Mantén los .proto versionados por paquete (v1, v2) para cambios breaking.
Checklist
- protoc genera sin errores
- Archivos .pb.go en el repo (o generados en CI)
- Esquema OpenAPI generado
5) Implementación del servidor
Archivo internal/interceptors/interceptors.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
package interceptors import ( "context" "strings" "time" "github.com/golang-jwt/jwt/v5" "go.uber.org/zap" "golang.org/x/time/rate" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) type AuthConfig struct{ JWTSecret string } func Unary(logger *zap.Logger, rl *rate.Limiter, auth AuthConfig) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { start := time.Now() defer func() { if r := recover(); r != nil { logger.Error("panic recovered", zap.Any("panic", r), zap.String("method", info.FullMethod)) } }() if rl != nil && !rl.Allow() { return nil, status.Error(codes.ResourceExhausted, "rate limit exceeded") } if strings.Contains(info.FullMethod, "CreateUser") { if err := authorize(ctx, auth.JWTSecret, []string{"admin"}); err != nil { return nil, err } } resp, err := handler(ctx, req) logger.Info("rpc", zap.String("method", info.FullMethod), zap.Duration("dur", time.Since(start)), zap.Error(err)) return resp, err } } func authorize(ctx context.Context, secret string, roles []string) error { md, ok := metadata.FromIncomingContext(ctx) if !ok { return status.Error(codes.Unauthenticated, "no metadata") } authz := md.Get("authorization") if len(authz) == 0 { return status.Error(codes.Unauthenticated, "no token") } tokenStr := strings.TrimPrefix(authz[0], "Bearer ") token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { return []byte(secret), nil }) if err != nil || !token.Valid { return status.Error(codes.Unauthenticated, "invalid token") } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return status.Error(codes.PermissionDenied, "invalid claims") } // autorización básica por rol if need := roles; len(need) > 0 { userRoles, _ := claims["roles"].([]interface{}) roleSet := map[string]struct{}{} for _, r := range userRoles { roleSet[r.(string)] = struct{}{} } for _, r := range need { if _, ok := roleSet[r]; ok { return nil } } return status.Error(codes.PermissionDenied, "forbidden") } return nil } |
Explicación: interceptor unary con logging, recuperación de pánicos, autorización JWT por roles y limitación de tasa tipo token bucket. Puedes añadir un interceptor stream similar.
Archivo internal/service/user_service.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
package service import ( "context" "time" userv1 "github.com/tuorg/grpc-go-microservice/internal/proto/user/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type UserRepo interface { Create(ctx context.Context, email, name string) (string, error) ListStream(ctx context.Context, limit int, ch chan<- *userv1.User) error } type UserService struct { userv1.UnimplementedUserServiceServer repo UserRepo } func NewUserService(r UserRepo) *UserService { return &UserService{repo: r} } func (s *UserService) CreateUser(ctx context.Context, in *userv1.CreateUserRequest) (*userv1.User, error) { if in.GetEmail() == "" { return nil, status.Error(codes.InvalidArgument, "email required") } select { case <-ctx.Done(): return nil, status.Error(codes.DeadlineExceeded, "request canceled") default: } id, err := s.repo.Create(ctx, in.Email, in.Name) if err != nil { return nil, status.Errorf(codes.Internal, "db error: %v", err) } return &userv1.User{Id: id, Email: in.Email, Name: in.Name, CreatedAt: timestamppb.New(time.Now())}, nil } func (s *UserService) WatchUsers(in *userv1.WatchUsersRequest, stream userv1.UserService_WatchUsersServer) error { ch := make(chan *userv1.User, 16) ctx := stream.Context() go func() { _ = s.repo.ListStream(ctx, int(in.GetLimit()), ch); close(ch) }() for { select { case <-ctx.Done(): return status.Error(codes.Canceled, "client canceled") case u, ok := <-ch: if !ok { return nil } if err := stream.Send(u); err != nil { return err } } } } |
Explicación: implementación básica del servicio con validación, manejo de context y propagación de errores gRPC idiomáticos.
Archivo cmd/server/main.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
package main import ( "context" "crypto/tls" "crypto/x509" "net" "net/http" "os" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" "golang.org/x/time/rate" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" "github.com/tuorg/grpc-go-microservice/internal/config" "github.com/tuorg/grpc-go-microservice/internal/interceptors" userv1 "github.com/tuorg/grpc-go-microservice/internal/proto/user/v1" "github.com/tuorg/grpc-go-microservice/internal/repo" "github.com/tuorg/grpc-go-microservice/internal/service" ) func main() { cfg := config.Load() logger, _ := zap.NewProduction() defer logger.Sync() // TLS/mTLS creds := loadTLS(cfg) rl := rate.NewLimiter(100, 200) // 100 req/s, burst 200 unary := interceptors.Unary(logger, rl, interceptors.AuthConfig{JWTSecret: cfg.JWTSecret}) grpcServer := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(unary)) repository := repo.NewPostgresRepo(cfg.DBDSN) us := service.NewUserService(repository) userv1.RegisterUserServiceServer(grpcServer, us) hs := health.NewServer() grpc_health_v1.RegisterHealthServer(grpcServer, hs) reflection.Register(grpcServer) go exposeMetricsAndHealth(cfg) go runHTTPGateway(cfg) ln, _ := net.Listen("tcp", cfg.GRPCAddr) logger.Info("gRPC listening", zap.String("addr", cfg.GRPCAddr)) _ = grpcServer.Serve(ln) } func loadTLS(cfg config.Config) credentials.TransportCredentials { cert, _ := tls.LoadX509KeyPair(cfg.TLSCert, cfg.TLSKey) caCert, _ := os.ReadFile(cfg.CACert) pool := x509.NewCertPool(); pool.AppendCertsFromPEM(caCert) tlsCfg := &tls.Config{Certificates: []tls.Certificate{cert}, ClientCAs: pool, ClientAuth: tls.RequireAndVerifyClientCert} return credentials.NewTLS(tlsCfg) } func exposeMetricsAndHealth(cfg config.Config) { http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK); _, _ = w.Write([]byte("ok")) }) _ = http.ListenAndServe(cfg.MetricsAddr, nil) } func runHTTPGateway(cfg config.Config) { // Se implementa abajo en la sección de Gateway } |
Explicación: registramos el servicio, interceptores, health y reflection; configuramos TLS/mTLS, un endpoint de métricas Prometheus y healthz, y placeholders para el gateway HTTP.
Checklist
- Interceptores implementados
- Servidor gRPC con TLS y health
- Logging configurado
6) Cliente gRPC con timeouts, retries y balanceo
Archivo internal/client/client.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package client import ( "context" "crypto/tls" "crypto/x509" "os" "time" userv1 "github.com/tuorg/grpc-go-microservice/internal/proto/user/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) type Client struct{ User userv1.UserServiceClient; conn *grpc.ClientConn } func New(addr, caFile string) (*Client, error) { pem, _ := os.ReadFile(caFile) pool := x509.NewCertPool(); pool.AppendCertsFromPEM(pem) creds := credentials.NewTLS(&tls.Config{RootCAs: pool}) svcCfg := `{ "loadBalancingConfig": [ { "round_robin": {} } ], "methodConfig": [{ "name": [{"service": "user.v1.UserService"}], "retryPolicy": {"MaxAttempts": 4, "InitialBackoff": "0.2s", "MaxBackoff": "2s", "BackoffMultiplier": 2, "RetryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]} }] }` conn, err := grpc.Dial( addr, grpc.WithTransportCredentials(creds), grpc.WithDefaultServiceConfig(svcCfg), grpc.WithBlock(), grpc.WithTimeout(5*time.Second), ) if err != nil { return nil, err } return &Client{User: userv1.NewUserServiceClient(conn), conn: conn}, nil } func (c *Client) Close() { _ = c.conn.Close() } |
Explicación: el cliente usa TLS, política de reintentos, backoff exponencial y balanceo round_robin. Se bloquea al conectar con timeout.
Ejemplo de uso (unary y streaming):
1 2 3 4 5 6 7 8 9 10 11 |
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second); defer cancel() cl, _ := client.New("dns:///api:50051", "certs/ca.crt") resp, err := cl.User.CreateUser(ctx, &userv1.CreateUserRequest{Email:"a@b.com", Name:"Alice"}) _ = err; _ = resp stream, _ := cl.User.WatchUsers(context.Background(), &userv1.WatchUsersRequest{Limit: 10}) for { u, err := stream.Recv(); if err != nil { break } _ = u } |
Explicación: mostramos llamadas unary y server streaming. Nota el esquema dns:/// para balanceo.
Checklist
- Cliente con TLS, reintentos y balanceo
- Ejemplos unary y streaming verificados
7) Seguridad: TLS y mTLS, JWT y rate limiting
7.1 Certificados autofirmados
1 2 3 4 5 6 7 8 9 10 11 12 13 |
mkdir -p certs # CA openssl genrsa -out certs/ca.key 4096 openssl req -x509 -new -nodes -key certs/ca.key -sha256 -days 3650 -out certs/ca.crt -subj "/CN=Local CA" # Server openssl genrsa -out certs/server.key 2048 openssl req -new -key certs/server.key -out certs/server.csr -subj "/CN=api" openssl x509 -req -in certs/server.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial -out certs/server.crt -days 365 -sha256 # Client (mTLS) openssl genrsa -out certs/client.key 2048 openssl req -new -key certs/client.key -out certs/client.csr -subj "/CN=client" openssl x509 -req -in certs/client.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial -out certs/client.crt -days 365 -sha256 |
Explicación: generas CA, certificados de servidor y cliente para habilitar TLS y mTLS en desarrollo.
7.2 JWT en metadata
1 2 3 4 5 |
// Cliente md := metadata.Pairs("authorization", "Bearer "+token) ctx := metadata.NewOutgoingContext(context.Background(), md) _, _ = cl.User.CreateUser(ctx, &userv1.CreateUserRequest{Email:"a@b.com", Name:"Alice"}) |
Explicación: los JWT viajan en la metadata gRPC, encabezado Authorization: Bearer.
Checklist
- Certificados creados y montados
- mTLS activado en servidor y cliente
- Interceptor JWT activo
8) Persistencia con PostgreSQL y migraciones
Usaremos GORM para rapidez y golang-migrate para migraciones.
Migración SQL (migrations/0001_init.sql):
1 2 3 4 5 6 7 |
CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); |
Explicación: crea una tabla users con ID UUID y restricciones básicas.
Repositorio GORM (internal/repo/postgres.go):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
package repo import ( "context" "time" "gorm.io/driver/postgres" "gorm.io/gorm" ) type User struct { ID string `gorm:"type:uuid;default:gen_random_uuid();primaryKey"` Email string `gorm:"uniqueIndex"` Name string CreatedAt time.Time } type PostgresRepo struct{ db *gorm.DB } func NewPostgresRepo(dsn string) *PostgresRepo { db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { panic(err) } return &PostgresRepo{db: db} } func (r *PostgresRepo) Create(ctx context.Context, email, name string) (string, error) { u := &User{Email: email, Name: name} if err := r.db.WithContext(ctx).Create(u).Error; err != nil { return "", err } return u.ID, nil } func (r *PostgresRepo) ListStream(ctx context.Context, limit int, ch chan<- *userv1.User) error { type row struct{ ID, Email, Name string; CreatedAt time.Time } rows, err := r.db.WithContext(ctx).Raw("SELECT id,email,name,created_at FROM users ORDER BY created_at DESC LIMIT ?", limit).Rows() if err != nil { return err } defer rows.Close() for rows.Next() { var rr row; _ = rows.Scan(&rr.ID, &rr.Email, &rr.Name, &rr.CreatedAt) ch <- &userv1.User{Id: rr.ID, Email: rr.Email, Name: rr.Name, CreatedAt: timestamppb.New(rr.CreatedAt)} } return nil } |
Explicación: abrimos el pool de conexiones, implementamos Create y un stream para listar usuarios. Usa WithContext para cancelación y deadlines.
Checklist
- Migraciones aplicadas con golang-migrate
- Repo implementado y probado
- Consultas paginadas disponibles (agrega LIMIT/OFFSET o cursor)
9) Observabilidad: OpenTelemetry y Prometheus
Inicialización OTel (internal/observability/otel.go):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package observability import ( "context" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/sdk/resource" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/sdk/trace" ) func InitOTEL(ctx context.Context, svcName, endpoint string) (func(context.Context) error, error) { exp, err := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint(endpoint), otlptracehttp.WithInsecure()) if err != nil { return nil, err } rsrc := resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceName(svcName)) tp := trace.NewTracerProvider(trace.WithBatcher(exp), trace.WithResource(rsrc)) otel.SetTracerProvider(tp) return tp.Shutdown, nil } |
Explicación: configuramos el exporter OTLP vía HTTP para enviar trazas a OTEL Collector.
Para gRPC, añade interceptores instrumentados (server/cliente) con go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.
Checklist
- Trazas exportadas a OTLP
- /metrics expuesto para Prometheus
- Endpoints /healthz y gRPC Health activos
10) Documentación y gateway REST con gRPC-Gateway
Implementación del gateway HTTP (cmd/server/main.go, función runHTTPGateway):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import ( "context" "net/http" "time" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" userv1 "github.com/tuorg/grpc-go-microservice/internal/proto/user/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func runHTTPGateway(cfg config.Config) { ctx, cancel := context.WithCancel(context.Background()); defer cancel() mux := runtime.NewServeMux( runtime.WithErrorHandler(runtime.DefaultHTTPErrorHandler), ) opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} _ = userv1.RegisterUserServiceHandlerFromEndpoint(ctx, mux, cfg.GRPCAddr, opts) httpSrv := &http.Server{Addr: cfg.HTTPAddr, Handler: mux, ReadHeaderTimeout: 5 * time.Second} _ = httpSrv.ListenAndServe() } |
Explicación: exponemos REST traduciendo JSON<->gRPC y mapeando errores gRPC a HTTP automáticamente.
Checklist
- Gateway sirviendo en HTTP_ADDR
- OpenAPI generado y servido (puedes servirlo con swagger-ui)
11) Pruebas: unitarias, integración y mocks
Prueba unitaria con testify (internal/service/user_service_test.go):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package service_test import ( "context" "testing" "time" "github.com/stretchr/testify/require" userv1 "github.com/tuorg/grpc-go-microservice/internal/proto/user/v1" "github.com/tuorg/grpc-go-microservice/internal/service" ) type fakeRepo struct{} func (f *fakeRepo) Create(ctx context.Context, email, name string) (string, error) { return "id-123", nil } func (f *fakeRepo) ListStream(ctx context.Context, limit int, ch chan<- *userv1.User) error { close(ch); return nil } func TestCreateUser_OK(t *testing.T) { s := service.NewUserService(&fakeRepo{}) ctx, cancel := context.WithTimeout(context.Background(), time.Second); defer cancel() u, err := s.CreateUser(ctx, &userv1.CreateUserRequest{Email:"a@b.com", Name:"Alice"}) require.NoError(t, err) require.Equal(t, "a@b.com", u.Email) } |
Explicación: probamos la ruta feliz con un repositorio falso. Para mocks complejos, usa gomock y genera interfaces.
Pruebas E2E con testcontainers-go (esqueleto):
1 2 |
// Levanta Postgres, aplica migraciones, arranca el binario y ejecuta llamadas gRPC con grpcurl o el cliente Go. |
Explicación: automatiza un entorno real para validar integración completa.
Checklist
- Unit tests passing
- Mocks generados con gomock cuando aplique
- E2E en CI opcional
12) Contenedorización y orquestación local
Dockerfile multi-stage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Builder FROM golang:1.22 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/server ./cmd/server # Runtime FROM gcr.io/distroless/base-debian11 WORKDIR / COPY --from=builder /bin/server /server COPY certs /certs ENV GRPC_ADDR=:50051 HTTP_ADDR=:8080 METRICS_ADDR=:2112 EXPOSE 50051 8080 2112 USER 65532:65532 ENTRYPOINT ["/server"] |
Explicación: compilamos estaticamente y usamos una imagen distroless mínima para producción.
docker-compose.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
version: "3.9" services: db: image: postgres:15 environment: POSTGRES_PASSWORD: pass POSTGRES_USER: user POSTGRES_DB: app ports: ["5432:5432"] healthcheck: test: ["CMD-SHELL", "pg_isready -U user"] interval: 5s timeout: 3s retries: 5 api: build: . environment: DB_DSN: postgres://user:pass@db:5432/app?sslmode=disable JWT_SECRET: supersecret TLS_CERT_FILE: /certs/server.crt TLS_KEY_FILE: /certs/server.key CA_CERT_FILE: /certs/ca.crt volumes: - ./certs:/certs:ro depends_on: db: condition: service_healthy ports: - "50051:50051" - "8080:8080" - "2112:2112" healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:2112/healthz"] interval: 10s timeout: 3s retries: 5 prometheus: image: prom/prometheus volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro ports: ["9090:9090"] |
Explicación: orquestamos API, Postgres y Prometheus. La API expone gRPC, REST y métricas con healthcheck.
Kubernetes (opcional, Deployment + Service):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
apiVersion: apps/v1 kind: Deployment metadata: { name: user-api } spec: replicas: 2 selector: { matchLabels: { app: user-api } } template: metadata: { labels: { app: user-api } } spec: containers: - name: api image: tuorg/user-api:latest ports: - { containerPort: 50051 } - { containerPort: 8080 } env: - { name: DB_DSN, valueFrom: { secretKeyRef: { name: db-dsn, key: dsn } } } --- apiVersion: v1 kind: Service metadata: { name: user-api } spec: selector: { app: user-api } ports: - { port: 50051, targetPort: 50051, name: grpc } - { port: 8080, targetPort: 8080, name: http } type: ClusterIP |
Explicación: despliegue con 2 réplicas y servicio interno. ConfigMaps/Secrets deben manejar parámetros sensibles.
Checklist
- Dockerfile multi-stage listo
- Compose con healthchecks
- Manifiestos K8s base disponibles
13) Rendimiento y buenas prácticas
- Limita goroutines y usa buffers en streams para backpressure.
- Activa compresión: gzip en cliente/servidor para mensajes grandes.
- Ajusta keepalive y tamaño de mensajes según payload.
- Reutiliza conexiones (pooling de clientes) y usa timeouts/cancelación siempre.
Ejemplo de keepalive y tamaños:
1 2 3 4 5 |
grpc.NewServer( grpc.MaxRecvMsgSize(8<<20), // 8 MiB grpc.MaxSendMsgSize(8<<20), ) |
Explicación: evita OOM limitando tamaños de mensajes y mejora la robustez de la comunicación.
Checklist
- Timeouts y deadlines en todas las llamadas
- Compresión activada donde aplica
- Límites de recursos definidos
14) Documentación final, próximos pasos y SEO
Checklist global
- Requisitos instalados y verificados
- .proto definidos con annotations y versionado
- Código generado y compilado
- Servidor con interceptores, TLS/mTLS y logging
- Cliente con reintentos y balanceo
- Persistencia con migraciones y repositorio
- Observabilidad con OpenTelemetry y Prometheus
- Gateway REST funcional y OpenAPI
- Pruebas unitarias e integración
- Docker Compose operativo con healthchecks
Próximos pasos
- Sharding y particionado de datos por dominio
- Service Mesh (Istio/Linkerd) para mTLS, políticas y telemetría avanzada
- Circuit breaking con proxy sidecar (Envoy)
- Feature flags y rollout progresivo
Preguntas frecuentes (FAQ)
- ¿Puedo mezclar gRPC y REST? Sí, con gRPC Gateway expones REST y mantienes un contrato único en .proto.
- ¿Cómo mapear errores gRPC a HTTP? El gateway mapea codes a 4xx/5xx; usa google.rpc.Status para detalles.
- ¿Es obligatorio mTLS? En producción es recomendado; al menos TLS unilateral.
- ¿Qué framework usar para validación? Puedes integrar protoc-gen-validate para validar mensajes .proto.
- ¿Cómo versiono APIs? Paquetes por versión (user.v1, user.v2) y mantener compatibilidad backward cuando sea posible.
15) Resolución de problemas comunes
- protoc: plugin not found: Asegúrate de que GOPATH/bin esté en PATH y que protoc-gen-go esté instalado con la misma versión del runtime.
- error loading shared libraries: Usa imágenes base compatibles o compila con CGO_ENABLED=0 para runtime distroless.
- x509: certificate signed by unknown authority: Añade la CA al trust store del cliente o desactiva temporalmente la verificación en dev (no recomendado).
- permission denied (JWT): Verifica roles y tiempos de expiración (exp) del token.
- deadline exceeded: Incrementa timeouts o optimiza consultas/índices en DB.
- UNAVAILABLE en balanceo: Usa dns:/// y múltiples endpoints; verifica health del servidor.
- gRPC Gateway 404: Confirma que la ruta y annotations coinciden, y que Register…HandlerFromEndpoint apunta al puerto correcto.
- OpenAPI vacío: Asegúrate de incluir los imports de annotations y usar protoc-gen-openapiv2.
- Conexiones a Postgres saturadas: Ajusta max_open_conns/max_idle_conns en GORM y usa context para cancelar queries largas.
- Falta de trazas: Verifica OTEL_EXPORTER_OTLP_ENDPOINT y que el collector esté escuchando.