Qué significa “rendimiento” en Go (y por qué medirlo)
En Go, el rendimiento suele estar dominado por tres factores: uso de CPU, presión de memoria (asignaciones y recolección de basura) y contención (bloqueos, espera en canales, syscalls, red). Optimizar sin medir suele empeorar el código o mover el problema a otro lugar. La práctica recomendada es: definir una métrica (latencia p95/p99, throughput, uso de CPU, RSS), capturar perfiles, proponer un cambio pequeño, volver a medir y comparar.
Profiling con pprof: CPU, heap, goroutines y bloqueo
Instrumentación básica en servicios
La forma más directa de perfilar un servicio es exponer endpoints de net/http/pprof en un puerto interno. Esto permite capturar perfiles en caliente sin detener el proceso.
import ( "net/http" _ "net/http/pprof" "log")func startPprof() { go func() { // Idealmente en localhost o red interna, con control de acceso. log.Println(http.ListenAndServe("127.0.0.1:6060", nil)) }()}Endpoints útiles: /debug/pprof/profile (CPU), /debug/pprof/heap (memoria), /debug/pprof/goroutine (stacks), /debug/pprof/block (bloqueos), /debug/pprof/mutex (contención de mutex). Para block y mutex necesitas habilitar muestreo.
import "runtime"func enableContentionProfiling() { runtime.SetBlockProfileRate(10000) // 1 muestra cada ~10µs bloqueado (ajusta según overhead) runtime.SetMutexProfileFraction(10) // 1 de cada 10 eventos de contención (ajusta) }Capturar y analizar perfil de CPU (paso a paso)
1) Captura 30s de CPU (ajusta duración para cubrir carga real):
go tool pprof -http=:0 http://127.0.0.1:6060/debug/pprof/profile?seconds=302) En la UI, revisa:
Top(funciones más costosas),Flame Graph(rutas calientes),Graph(relaciones).Continúa en nuestra aplicación.- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
3) Interpreta “flat” vs “cum”:
flates tiempo en esa función;cumincluye llamadas descendientes. Uncumalto conflatbajo suele indicar que la función orquesta trabajo pesado en otras.4) Repite con el mismo escenario tras un cambio y compara (ver sección “medir antes/después”).
Heap profile: asignaciones, retención y GC
El heap profile te ayuda a responder: ¿qué asigna más?, ¿qué objetos se retienen?, ¿estoy creando basura innecesaria? Importante: el heap puede verse de dos maneras: en uso (objetos vivos) y asignado (total asignado a lo largo del tiempo). Para encontrar “churn” (mucha basura), mira asignaciones; para fugas/retención, mira en uso.
Captura heap:
go tool pprof -http=:0 http://127.0.0.1:6060/debug/pprof/heapConsejo: en la UI, cambia entre
inuse_space,inuse_objects,alloc_space,alloc_objectspara ver perspectivas distintas.
Goroutines: detectar acumulación y puntos de espera
El perfil de goroutines muestra stacks de todas las goroutines. Es clave para detectar: goroutines que se quedan esperando para siempre, fugas por canales no drenados, o explosión de concurrencia.
go tool pprof -http=:0 http://127.0.0.1:6060/debug/pprof/goroutineBusca patrones repetidos (muchas goroutines con el mismo stack) y estados como chan receive, select, sync.Mutex.Lock, IO wait.
Bloqueo y contención: block y mutex
Si la CPU no está alta pero la latencia sí, suele haber espera: locks, canales, syscalls o red. Los perfiles de bloqueo ayudan a cuantificar dónde se pierde tiempo esperando.
go tool pprof -http=:0 http://127.0.0.1:6060/debug/pprof/blockgo tool pprof -http=:0 http://127.0.0.1:6060/debug/pprof/mutexInterpreta con cuidado: habilitar estos perfiles tiene overhead. Úsalos en entornos de staging o ventanas controladas en producción.
Asignaciones, escapes y cómo reducir presión de memoria
Asignaciones: el costo oculto
Cada asignación en heap incrementa trabajo del GC. En cargas altas, reducir asignaciones suele mejorar latencia p99 más que micro-optimizaciones de CPU. Señales típicas: heap con alloc_space alto, GC frecuente, y perfiles con muchas llamadas a make, new, conversiones a string o concatenaciones.
Escape analysis: cuándo algo se va al heap
Go decide si una variable vive en stack o heap. Si “escapa” (por ejemplo, se devuelve un puntero, se guarda en una interfaz, o el compilador no puede probar el alcance), se asigna en heap. Puedes inspeccionar escapes con flags del compilador.
go build -gcflags="all=-m=2" ./...Busca mensajes como escapes to heap. No se trata de eliminar todos los escapes, sino de entender los que ocurren en rutas calientes.
Reuso de buffers: menos basura, más throughput
Patrón común: construir respuestas o payloads con buffers reutilizables. Dos herramientas típicas: bytes.Buffer (con Grow) y sync.Pool para reusar objetos temporales bajo carga.
var bufPool = sync.Pool{ New: func() any { b := make([]byte, 0, 32*1024) return &b },}func withBuffer(fn func([]byte) []byte) []byte { bptr := bufPool.Get().(*[]byte) b := (*bptr)[:0] out := fn(b) // Importante: no guardar referencias a b tras devolverlo al pool. *bptr = b[:0] bufPool.Put(bptr) return out}- Cuándo usar
sync.Pool: objetos temporales, alta tasa de asignación, vida corta, y cuando el GC está siendo un problema. - Cuándo evitarlo: si complica el código, si hay riesgo de retener buffers enormes, o si el beneficio no se demuestra con perfiles.
Preasignación de slices y mapas
Si conoces tamaños aproximados, preasigna capacidad para evitar realocaciones y copias.
// Slice: evita crecimientos repetidosres := make([]Item, 0, n)// Map: reduce rehashingm := make(map[string]int, n)Costo de conversiones: []byte ↔ string y concatenaciones
Convertir entre []byte y string normalmente implica copiar datos (asignación + copia). En rutas calientes (parsers, middlewares, logging), esto puede dominar el perfil de heap.
- Evita concatenar strings en bucles; usa
strings.BuilderyGrowsi puedes estimar tamaño.
var b strings.Builderb.Grow(1024)for _, s := range parts { b.WriteString(s) b.WriteByte(',')}out := b.String()- Evita convertir repetidamente: si una API interna puede operar en
[]byteo enstring, elige una representación y manténla.
Nota: existen técnicas “zero-copy” con unsafe, pero suelen ser frágiles y no recomendables en código general; prioriza claridad y mide si realmente lo necesitas.
Patrones de optimización sin sacrificar legibilidad
Optimiza primero lo que el perfil señala
Regla práctica: si una función no aparece en el top del perfil, rara vez vale la pena micro-optimizarla. En cambio, enfócate en: reducir asignaciones en rutas calientes, evitar trabajo repetido, y disminuir contención.
Evita trabajo repetido: cache local y precomputación
- Compilar regex una sola vez (global o inyectado) en lugar de por request.
- Precalcular estructuras derivadas (tablas, índices) si el input cambia poco.
- Evitar formateos costosos en logs en nivel deshabilitado (usa logging estructurado con evaluación perezosa si aplica).
Reduce contención: sharding y minimizar secciones críticas
Si un mutex aparece en el perfil, prueba: reducir el trabajo dentro del lock, usar estructuras por shard (por ejemplo, N mapas con N locks), o usar canales/buffers para serializar solo lo necesario. No cambies a estructuras “lock-free” sin evidencia: suelen complicar y no siempre mejoran.
Batching y amortización
En cargas altas, agrupar operaciones reduce overhead: escribir en lote, procesar eventos en batches, o usar buffers para I/O. Mide la latencia: batching puede aumentar p99 si el tamaño/intervalo no está bien ajustado.
Cómo medir antes/después (benchmarks y comparación de perfiles)
Microbenchmarks con go test -bench
Para cambios locales (funciones, codecs, parsers), usa benchmarks y reporta asignaciones.
go test ./... -bench=BenchmarkMiFuncion -benchmem -run=^$Interpreta: ns/op (tiempo), B/op (bytes asignados), allocs/op (número de asignaciones). Un objetivo común es bajar allocs/op en rutas calientes.
Comparar perfiles con pprof (diff)
Guarda perfiles “antes” y “después” bajo el mismo escenario de carga.
# CPU antescurl -o cpu_before.pb.gz "http://127.0.0.1:6060/debug/pprof/profile?seconds=30"# CPU despuéscurl -o cpu_after.pb.gz "http://127.0.0.1:6060/debug/pprof/profile?seconds=30"# Comparación (pprof permite base)go tool pprof -http=:0 -base cpu_before.pb.gz cpu_after.pb.gzHaz lo mismo con heap si el cambio apunta a asignaciones. Asegúrate de que el tráfico, configuración y versión de Go sean comparables.
Medición a nivel servicio: latencia y recursos
Para cambios en un servicio, mide con un generador de carga reproducible y observa: latencia p50/p95/p99, throughput, CPU, RSS, GC (pausas y frecuencia), y número de goroutines. Un cambio “bueno” suele mejorar p99 o estabilizar memoria bajo carga sostenida.
Recomendaciones para servicios de alta carga
Límites de concurrencia (no todo debe ser una goroutine)
Crear una goroutine por unidad de trabajo puede saturar CPU, memoria y el scheduler si el input crece. Usa límites explícitos: semáforos (canal con buffer) o pools de workers.
// Semáforo para limitar concurrencia a Nvar sem = make(chan struct{}, N)func doWork(item Item) { sem <- struct{}{} go func() { defer func() { <-sem }() process(item) }()}En endpoints, considera colas internas con tamaño máximo; si se llenan, aplica backpressure (rechazar o degradar).
Timeouts y cancelación
En alta carga, los timeouts evitan acumulación de trabajo inútil. Aplica timeouts en llamadas a dependencias (DB, HTTP) y propaga cancelación. Asegúrate de que goroutines internas respeten el contexto para no fugar trabajo.
Backpressure: proteger el sistema bajo presión
- Colas acotadas: canales con buffer limitado para desacoplar picos sin crecer sin límite.
- Rechazo temprano: si el sistema está saturado, responde con error controlado (por ejemplo, 429/503) en lugar de degradar todo.
- Degradación: desactivar features costosas (cálculos extra, enriquecimiento) cuando se supera un umbral.
Uso prudente de concurrencia
- Evita fan-out masivo (lanzar miles de goroutines) sin límite; prefiere pools o semáforos.
- Evita locks globales en rutas calientes; usa sharding o reduce la sección crítica.
- Observa goroutines: un crecimiento sostenido suele indicar bloqueo, fugas o falta de backpressure.
Checklist práctico para una sesión de optimización
| Objetivo | Qué capturar | Qué buscar |
|---|---|---|
| CPU alta | CPU pprof | Top funciones, flame graph, trabajo repetido |
| Memoria/GC alto | Heap (alloc/inuse) | allocs/op, conversiones, buffers, retención |
| Latencia alta con CPU moderada | block/mutex + goroutine | contención, esperas en canales, IO wait |
| Degradación bajo carga | métricas + goroutines | colas sin límite, falta de timeouts, fan-out |