go generate en Go: stringer, mockgen, sqlc y generación de código como parte del build

go generate es un comando que ejecuta programas arbitrarios a partir de directivas especiales en los comentarios del código fuente. Su propósito es automatizar la generación de código: métodos String() para tipos enumerados, mocks de interfaces, código SQL tipado y código de Protocol Buffers. Todo esto como parte del proceso de build.

Cómo funciona go generate

//go:generate comando argumentos

Al ejecutar go generate ./..., Go busca estas directivas en los ficheros .go del proyecto y ejecuta los comandos. Habitualmente se concentran en un fichero generate.go en la raíz del paquete.

stringer: métodos String() para tipos enumerados

// estado.go
package domain

//go:generate stringer -type=EstadoPedido -output=estado_string.go

type EstadoPedido int

const (
    EstadoPendiente EstadoPedido = iota
    EstadoProcesando
    EstadoEnviado
    EstadoCancelado
)

// Después de go generate, puedes usar:
// fmt.Println(EstadoPendiente) ? "EstadoPendiente"
// e.String()                  ? "EstadoEnviado"
go install golang.org/x/tools/cmd/stringer@latest
go generate ./internal/domain/

El fichero estado_string.go generado contiene un array constante de cadenas con una búsqueda O(1). Es más eficiente que un switch manual y se mantiene sincronizado automáticamente al agregar o quitar valores del enum.

mockgen: mocks automáticos de interfaces

// generate.go
package ports

//go:generate mockgen -source=repositories.go -destination=../mocks/mock_repositories.go -package=mocks

// En los tests:
import (
    "testing"
    "github.com/golang/mock/gomock"
    "tuapp/internal/mocks"
)

func TestCrearUsuario(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mocks.NewMockUsuarioRepository(ctrl)
    mockEmail := mocks.NewMockEmailService(ctrl)

    // Configurar expectativas
    mockRepo.EXPECT().
        BuscarPorEmail("[email protected]").
        Return(nil, nil)
    mockRepo.EXPECT().
        Guardar(gomock.Any()).
        Return(nil)

    uc := application.NewCrearUsuarioUseCase(mockRepo, mockEmail)
    out, err := uc.Ejecutar(application.CrearUsuarioInput{
        Nombre: "Ana",
        Email:  "[email protected]",
    })

    if err != nil {
        t.Fatalf("error inesperado: %v", err)
    }
    if out.Email != "[email protected]" {
        t.Errorf("email incorrecto: %s", out.Email)
    }
}

sqlc: queries SQL tipadas

sqlc lee tus queries SQL y genera código Go tipado para cada una. Nunca más interface{} en las respuestas de base de datos:

-- queries/usuarios.sql
-- name: ObtenerUsuario :one
SELECT id, nombre, email, activo FROM usuarios WHERE id = ? LIMIT 1;

-- name: ListarUsuarios :many
SELECT id, nombre, email FROM usuarios WHERE activo = true ORDER BY nombre LIMIT ? OFFSET ?;

-- name: CrearUsuario :execresult
INSERT INTO usuarios (nombre, email, activo, creado_en) VALUES (?, ?, true, NOW());
// generate.go
//go:generate sqlc generate

// El código generado:
type ObtenerUsuarioRow struct {
    ID     int32
    Nombre string
    Email  string
    Activo bool
}

func (q *Queries) ObtenerUsuario(ctx context.Context, id int32) (ObtenerUsuarioRow, error) {
    row := q.db.QueryRowContext(ctx, obtenerUsuario, id)
    var i ObtenerUsuarioRow
    err := row.Scan(&i.ID, &i.Nombre, &i.Email, &i.Activo)
    return i, err
}

buf: generar protobuf y gRPC

// generate.go del paquete proto
//go:generate buf generate

// buf.gen.yaml
version: v2
plugins:
  - plugin: buf.build/protocolbuffers/go
    out: gen/go
    opt: paths=source_relative
  - plugin: buf.build/grpc/go
    out: gen/go
    opt: paths=source_relative

Organizar las directivas en generate.go

La convención es crear un fichero generate.go (o doc.go) en cada paquete que necesite generación. Esto agrupa todas las directivas en un lugar visible y permite ejecutar go generate ./... desde la raíz:

// internal/ports/generate.go
// Package ports define las interfaces del dominio.
// Para regenerar los mocks ejecuta: go generate ./...
package ports

//go:generate mockgen -source=repositories.go -destination=../mocks/mock_repositories.go -package=mocks
//go:generate mockgen -source=services.go -destination=../mocks/mock_services.go -package=mocks

En el CI añade un paso que ejecuta go generate ./... y luego comprueba con git diff --exit-code que no hay ficheros generados sin commitear. Esto garantiza que el código generado siempre está sincronizado con el código fuente.

COMPARTE ESTE ARTÍCULO

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