Cuando tu aplicación en Go procesa miles de peticiones por segundo, usar log.Printf tiene un coste real: alocaciones de memoria en cada llamada y una salida de texto plano que nadie puede filtrar por campo. Zap de Uber (go.uber.org/zap) resuelve esto con logging estructurado en JSON, cero alocaciones en la ruta crítica y campos tipados desde el principio.
go get go.uber.org/zap
Los dos modos: Logger y SugaredLogger
Zap tiene dos constructores listos para usar. zap.NewProduction() emite JSON con nivel info. zap.NewDevelopment() usa texto coloreado con nivel debug:
package main
import "go.uber.org/zap"
func main() {
logger, err := zap.NewProduction()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info("servidor iniciado",
zap.String("host", "0.0.0.0"),
zap.Int("port", 8080),
)
logger.Warn("umbral de conexiones al 80%",
zap.Int("active", 800),
zap.Int("max", 1000),
)
}
// {"level":"info","ts":1719350000.123,"caller":"main/main.go:13","msg":"servidor iniciado","host":"0.0.0.0","port":8080}
SugaredLogger para código menos verboso
Si los campos tipados resultan demasiado verbosos, SugaredLogger permite pares clave/valor sin declarar el tipo. El coste extra son unos pocos nanosegundos por llamada:
base, _ := zap.NewDevelopment()
defer base.Sync()
sugar := base.Sugar()
sugar.Infow("usuario autenticado",
"user_id", 42,
"email", "[email protected]",
"role", "admin",
)
sugar.Infof("petición procesada en %dms, estado %d", 37, 200)
sugar.Errorf("fallo al conectar con la BD: %v", err)
La regla práctica: usa Logger en librerías y rutas de alta frecuencia, SugaredLogger en código de aplicación donde la comodidad importa más.
Campos contextuales con logger.With()
With() devuelve un nuevo logger con campos fijos que aparecen en todos sus mensajes. Perfecto para handlers HTTP donde quieres que el request_id aparezca en cada log:
func processOrder(base *zap.Logger, orderID string, userID int) error {
logger := base.With(
zap.String("order_id", orderID),
zap.Int("user_id", userID),
)
logger.Info("procesando pedido")
if err := checkInventory(orderID); err != nil {
logger.Error("stock insuficiente", zap.Error(err))
return err
}
logger.Info("pedido confirmado")
return nil
}
Personalización con zapcore: múltiples destinos
Cuando necesitas combinar salidas JSON en fichero y texto en consola entra en juego zapcore:
package main
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
fileEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
f, _ := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
fileSink := zapcore.AddSync(f)
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
consoleSink := zapcore.AddSync(os.Stderr)
core := zapcore.NewTee(
zapcore.NewCore(fileEncoder, fileSink, zapcore.InfoLevel),
zapcore.NewCore(consoleEncoder, consoleSink, zapcore.DebugLevel),
)
logger := zap.New(core, zap.AddCaller())
defer logger.Sync()
logger.Debug("modo debug activo") // solo consola
logger.Info("aplicación lista") // consola y fichero
}
zap.NewNop() en tests
func TestProcessOrder(t *testing.T) {
logger := zap.NewNop() // descarta todo sin I/O
err := processOrder(logger, "ORD-001", 42)
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
}
El antipatrón que destruye el rendimiento
El error más habitual al venir de log.Printf: usar fmt.Sprintf para montar el mensaje antes de pasárselo al logger:
// MAL: construye el string aunque el nivel debug esté desactivado en producción
logger.Debug(fmt.Sprintf("usuario %d accedió a %s", userID, path))
// BIEN: Zap evalúa el nivel antes de serializar nada
logger.Debug("acceso de usuario",
zap.Int("user_id", userID),
zap.String("path", path),
)
Con fmt.Sprintf pagas la alocación del string aunque el nivel debug esté desactivado en producción. Con campos tipados, Zap comprueba primero si el nivel está activo y solo entonces serializa. En rutas de alta frecuencia, esa diferencia se acumula rápidamente.
Comparativa rápida de rendimiento
- zap.Logger: ~100 ns/op sin alocaciones.
- zap.SugaredLogger: ~200 ns/op, pocas alocaciones.
- log/slog (estándar Go 1.21): ~300 ns/op, equilibrio entre comodidad y rendimiento.
- log.Printf estándar: ~1000 ns/op, alocaciones por reflexión.
