---
name: go-experto
description: >
  Go idiomático: concurrencia con goroutines y channels, context propagation,
  error handling, HTTP servers con chi/gin y graceful shutdown. Cargar cuando
  se implementen servicios Go, handlers HTTP o lógica concurrente.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
exclusiones:
  - "No cargar para diagnosticar errores de compilación Go o problemas de módulos — para build errors cargar `build-errors-go`."
  - "No cargar para diseñar la arquitectura de paquetes Go (interfaces, patrones) — para eso cargar `go-patrones`."
  - "No cargar para escribir tests en Go — para testing cargar `go-testing`."
  - "No cargar para lógica de backend que no sea Go — si el stack es Python, Java o Node.js cargar el skill correspondiente."
evolvable: true  # default para skill estandar
---
# Go Experto — Go Idiomático y Concurrencia

Cubre el desarrollo Go idiomático: net/http con routers, middleware, manejo
de context.Context, concurrencia con goroutines y channels, error handling
con wrapping, logging estructurado y graceful shutdown.

## Cuándo cargar este skill

Invoca `Skill("go-experto")` cuando:

- Se implementen HTTP handlers o middleware en Go
- Se use concurrencia con goroutines, channels o errgroup
- Se propague context.Context a través de capas
- Se diseñe el manejo de errores con sentinel errors o custom types
- Se configure graceful shutdown de un servidor HTTP
- Se implemente dependency injection sin frameworks

## Cuándo NO cargar

- La pregunta es sobre errores de compilación Go o problemas de `go.mod`/`go.sum` — para build errors cargar `build-errors-go`.
- La pregunta es sobre diseñar interfaces, patrones de paquetes o arquitectura — para diseño cargar `go-patrones`.
- La tarea es escribir tests unitarios o de integración en Go — cargar `go-testing`.
- El backend usa Python, Java, Node.js u otro lenguaje — este skill cubre Go específicamente.

## Conceptos clave

### context.Context es el primer parámetro — siempre

Context propaga cancelación, deadlines y valores a través de goroutines. Se
pasa como primer parámetro named `ctx context.Context`. Nunca almacenar en structs.

### Error handling explícito

Go no tiene excepciones. Cada error se verifica en el punto donde ocurre.
`errors.Is` compara por valor (sentinel), `errors.As` extrae por tipo.
`fmt.Errorf("operación: %w", err)` envuelve conservando la cadena.

### Goroutine leak prevention

Una goroutine sin mecanismo de terminación es un leak. Siempre proporcionar
forma de cancelar: context con cancel, channel de done o errgroup.

### Dependency injection por constructor

Los servicios reciben sus dependencias por constructor, no las instancian.
Favorece testabilidad e intercambiabilidad de implementaciones.

## Reglas obligatorias

### Verificar TODOS los errores — NUNCA ignorar con _

```go
// MAL
result, _ := db.Query(ctx, sql)

// BIEN
result, err := db.Query(ctx, sql)
if err != nil {
    return fmt.Errorf("consultar pedidos: %w", err)
}
```

### Cancelar SIEMPRE el context derivado

```go
// MAL — context con timeout sin defer cancel
ctx, _ := context.WithTimeout(parent, 5*time.Second)

// BIEN
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
```

### Usar errgroup para goroutines con errores

```go
// MAL — WaitGroup no propaga errores
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    if err := hacerAlgo(); err != nil {
        log.Println(err) // error perdido
    }
}()

// BIEN — errgroup cancela todas al primer error
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return hacerAlgo(ctx) })
g.Go(func() error { return hacerOtraCosa(ctx) })
if err := g.Wait(); err != nil {
    return fmt.Errorf("procesamiento paralelo: %w", err)
}
```

### context.Background() solo en main — NUNCA en handlers

Los handlers reciben el context del request (`r.Context()`). Crear
`context.Background()` en un handler rompe la propagacion de cancelacion
del cliente.

## Patrones recomendados

### HTTP server con chi y middleware

```go
func NuevoRouter(pedidoHandler *PedidoHandler, auth Middleware) http.Handler {
    r := chi.NewRouter()
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)

    r.Route("/v1", func(r chi.Router) {
        r.Use(auth.Verificar)
        r.Route("/pedidos", func(r chi.Router) {
            r.Get("/", pedidoHandler.Listar)
            r.Post("/", pedidoHandler.Crear)
            r.Get("/{id}", pedidoHandler.Obtener)
        })
    })
    return r
}
```

### Handler con context y error handling

```go
func (h *PedidoHandler) Crear(w http.ResponseWriter, r *http.Request) {
    var req CrearPedidoRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        responderError(w, http.StatusBadRequest, "body inválido")
        return
    }

    pedido, err := h.service.Crear(r.Context(), req)
    if err != nil {
        var notFound *NotFoundError
        switch {
        case errors.As(err, &notFound):
            responderError(w, http.StatusNotFound, notFound.Error())
        default:
            slog.ErrorContext(r.Context(), "crear pedido", "error", err)
            responderError(w, http.StatusInternalServerError, "error interno")
        }
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(pedido)
}
```

### Error types con sentinel y wrapping

```go
// Sentinel errors para comparación con errors.Is
var (
    ErrNotFound    = errors.New("recurso no encontrado")
    ErrUnauthorized = errors.New("no autorizado")
)

// Custom type para contexto con errors.As
type ValidationError struct {
    Campo   string
    Mensaje string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validación fallida en %s: %s", e.Campo, e.Mensaje)
}

// Wrapping que preserva la cadena
func (s *PedidoService) Crear(ctx context.Context, req CrearPedidoRequest) (*Pedido, error) {
    cliente, err := s.clienteRepo.Obtener(ctx, req.ClienteID)
    if err != nil {
        return nil, fmt.Errorf("obtener cliente %s: %w", req.ClienteID, err)
    }
    // ...
}
```

### Graceful shutdown

```go
func main() {
    srv := &http.Server{Addr: ":8080", Handler: nuevoRouter()}

    go func() {
        if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
            slog.Error("servidor fallo", "error", err)
            os.Exit(1)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        slog.Error("shutdown forzado", "error", err)
    }
}
```

### Structured logging con slog

```go
// Configuración en main
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))
slog.SetDefault(logger)

// Uso en handlers y services
slog.InfoContext(ctx, "pedido creado",
    "pedido_id", pedido.ID,
    "cliente_id", pedido.ClienteID,
    "total", pedido.Total,
)
```

## Anti-patrones conocidos

### Goroutine sin mecanismo de cancelación

```go
// MAL — goroutine que vive para siempre
go func() {
    for {
        procesarCola()
        time.Sleep(time.Second)
    }
}()

// BIEN — goroutine controlada por context
go func() {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            procesarCola(ctx)
        case <-ctx.Done():
            return
        }
    }
}()
```

### Panic en lugar de error return

```go
// MAL — panic en lógica de negocio
func dividir(a, b float64) float64 {
    if b == 0 { panic("división por cero") }
    return a / b
}

// BIEN — error explícito
func dividir(a, b float64) (float64, error) {
    if b == 0 { return 0, errors.New("división por cero") }
    return a / b, nil
}
```

## Checklist de verificación

- [ ] Todos los errores verificados, ningún `_` descartando error
- [ ] context.Context como primer parámetro en funciones que hacen I/O
- [ ] Toda goroutine tiene mecanismo de terminación (context, done channel)
- [ ] defer cancel() inmediatamente después de WithTimeout/WithCancel
- [ ] context.Background() solo en main o tests
- [ ] errgroup en lugar de WaitGroup cuando hay errores que propagar
- [ ] Graceful shutdown implementado en el servidor principal

## Gotchas / Errores comunes no obvios

**`defer cancel()` después de `context.WithTimeout` pero antes de verificar el error**: poner `ctx, cancel := context.WithTimeout(parent, 5*time.Second)` y llamar `defer cancel()` es correcto, pero si `cancel()` se llama antes de que la operación termine porque la función retornó por otro camino (un error temprano), el contexto se cancela innecesariamente para tareas downstream que aún lo usan. Fix: llamar `defer cancel()` inmediatamente después de `WithTimeout` — eso es correcto e intencional; lo que se debe evitar es pasar `ctx` a goroutines que viven más que la función que creó el contexto.

**Goroutine leak cuando el channel receptor termina antes de enviar**: `for _, item := range items { go func() { results <- procesar(item) }() }` — si el goroutine principal sale (por error o cancelación) mientras los goroutines aún intentan enviar a `results`, los goroutines quedan bloqueados para siempre. Fix: usar `select { case results <- r: case <-ctx.Done(): return }` dentro de cada goroutine para que puedan salir cuando se cancela el contexto.

**`errors.Is` no detecta errores envueltos con `fmt.Errorf("%v", err)`**: usar `%v` en el format string descarta la cadena de wrapping — el error resultante es un string, no un error envuelto. Solo `%w` preserva la cadena para `errors.Is`/`errors.As`. Fix: siempre usar `fmt.Errorf("operación falló: %w", err)` cuando se quiera que el error sea inspeccionable con `errors.Is` o `errors.As`.

**`sync.WaitGroup.Add()` llamado dentro de la goroutine en lugar de antes de lanzarla**: si `Add(1)` se llama dentro de `go func() { wg.Add(1); defer wg.Done(); ... }()`, existe una condición de carrera donde `wg.Wait()` puede retornar antes de que las goroutines hayan hecho `Add`. Fix: llamar siempre `wg.Add(1)` en el goroutine padre, justo antes de `go func()`.

---
*Skill creado con swl:crear-skill el 2026-03-31. Versión 1.0.0.*
