gRPC y Protocol Buffers en Go: definir servicios .proto, generar código y servidor/cliente

gRPC es el framework de llamadas a procedimiento remoto de Google y lleva Protocol Buffers como lenguaje de definición de interfaces. La combinación da un contrato tipado, transporte HTTP/2 y generación de código para el servidor y el cliente. Aquí se explica cómo montarlo de cero en Go.

Instalación de protoc y los plugins Go

Necesitas el compilador de Protocol Buffers (protoc) y dos plugins: uno para generar los mensajes Go y otro para generar el código gRPC:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

En Linux puedes instalar protoc con el gestor de paquetes de tu distribución o descargarlo desde el repositorio oficial de protocolbuffers. Asegúrate de que tanto protoc como los dos plugins estén en el PATH antes de continuar.

Definir el servicio en .proto

El fichero .proto describe los mensajes y los métodos que expone el servicio. Es el único punto de verdad que comparten cliente y servidor:

syntax = "proto3";

option go_package = "github.com/tuusuario/miservicio/proto;proto";

package greeter;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
  rpc GetUser  (UserRequest)  returns (UserReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

message UserRequest {
  int32 id = 1;
}

message UserReply {
  int32  id    = 1;
  string name  = 2;
  string email = 3;
}

Generar el código Go

Con el fichero guardado como proto/greeter.proto, ejecuta:

protoc --go_out=. --go_opt=paths=source_relative 
       --go-grpc_out=. --go-grpc_opt=paths=source_relative 
       proto/greeter.proto

Obtendrás dos ficheros: greeter.pb.go con los structs de mensajes y greeter_grpc.pb.go con las interfaces del servidor y el cliente stub. No edites esos ficheros a mano.

Implementar el servidor

El servidor tiene que implementar la interfaz GreeterServer generada. El patrón recomendado es embeber UnimplementedGreeterServer para que los métodos no implementados devuelvan un error gRPC correcto en lugar de un panic:

package main

import (
    "context"
    "fmt"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    pb "github.com/tuusuario/miservicio/proto"
)

type server struct {
    pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
    if req.Name == "" {
        return nil, status.Error(codes.InvalidArgument, "el nombre no puede estar vacío")
    }
    return &pb.HelloReply{Message: "Hola, " + req.Name}, nil
}

func (s *server) GetUser(ctx context.Context, req *pb.UserRequest) (*pb.UserReply, error) {
    // simulación: en producción aquí va la consulta a BBDD
    usuarios := map[int32]*pb.UserReply{
        1: {Id: 1, Name: "Ana García", Email: "[email protected]"},
        2: {Id: 2, Name: "Luis Martín", Email: "[email protected]"},
    }
    u, ok := usuarios[req.Id]
    if !ok {
        return nil, status.Errorf(codes.NotFound, "usuario %d no encontrado", req.Id)
    }
    return u, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("error al escuchar: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    fmt.Println("Servidor gRPC en :50051")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("error al servir: %v", err)
    }
}

Implementar el cliente

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    pb "github.com/tuusuario/miservicio/proto"
)

func main() {
    conn, err := grpc.NewClient("localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        log.Fatalf("no se pudo conectar: %v", err)
    }
    defer conn.Close()

    c := pb.NewGreeterClient(conn)

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

    reply, err := c.SayHello(ctx, &pb.HelloRequest{Name: "María"})
    if err != nil {
        log.Fatalf("error en SayHello: %v", err)
    }
    fmt.Println(reply.Message)

    user, err := c.GetUser(ctx, &pb.UserRequest{Id: 1})
    if err != nil {
        log.Fatalf("error en GetUser: %v", err)
    }
    fmt.Printf("Usuario: %s (%s)n", user.Name, user.Email)
}

Manejo de errores tipados

Los errores gRPC llevan un código de estado que el cliente puede inspeccionar para decidir qué hacer. El paquete google.golang.org/grpc/status lo gestiona:

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

user, err := c.GetUser(ctx, &pb.UserRequest{Id: 999})
if err != nil {
    st, ok := status.FromError(err)
    if ok && st.Code() == codes.NotFound {
        fmt.Println("usuario no encontrado, creándolo...")
    } else {
        log.Fatalf("error inesperado: %v", err)
    }
}

Interceptores de logging

Los interceptores son el equivalente gRPC del middleware HTTP. Se registran al crear el servidor y se ejecutan en cada llamada:

func logInterceptor(
    ctx context.Context,
    req any,
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (any, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    log.Printf("[gRPC] %s | duración=%s | error=%v",
        info.FullMethod, time.Since(start), err)
    return resp, err
}

s := grpc.NewServer(grpc.UnaryInterceptor(logInterceptor))

Para combinar varios interceptores sin importar librerías externas puedes encadenarlos manualmente, aunque paquetes como go-grpc-middleware facilitan la composición con grpc.ChainUnaryInterceptor.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP