CommunityProgramación y desarrollogithub.com

davidvella/go-style

Escribe, revisa o refactoriza código Go que sea fácil de leer y aún así rápido. Mantiene funciones pequeñas con baja complejidad ciclomática y cognitiva, pone el rendimiento en el diseño de la API y la disposición de los datos en lugar de en código ingenioso, y demuestra los cambios con benchmarks, conteos de asignaciones y análisis de escape.

¿Qué es go-style?

Este skill de agente de IA está diseñado para ayudar a desarrolladores a escribir, revisar o refactorizar código Go que priorice tanto la legibilidad como el rendimiento. Su filosofía central es que el rendimiento debe provenir del diseño de la API y la estructura de los datos, no de trucos de código obscuros. El skill guía al agente (o al desarrollador) a través de un conjunto de principios y prácticas concretas: separar caminos calientes (hot paths) de caminos fríos (cold paths), diseñar la API para minimizar asignaciones en el heap, mantener funciones pequeñas y planas con baja complejidad, y usar benchmarks y análisis de escape para respaldar las afirmaciones de rendimiento. Es útil para cualquier persona que trabaje en Go y quiera escribir código que sea mantenible y eficiente, aplicable en plataformas como Claude Code, Cursor o Codex. El skill se basa en las charlas y el código de Dave Cheney y los principios de John Ousterhout, y proporciona ejemplos y referencias adicionales en archivos markdown dentro del repositorio.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/davidvella/skills/tree/HEAD/skills/go-style

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Estilo Go: legible y rápido

El objetivo es código que un mantenedor pueda leer de arriba a abajo, con rendimiento que provenga de la forma de la API y de los datos, no de trucos repartidos por el código. La legibilidad y la velocidad rara vez entran en conflicto una vez que el diseño es correcto; cuando lo hacen, sacrifica la legibilidad solo en un camino caliente medido, y dilo en un comentario.

Esto sigue la charla de Dave Cheney "API Design as a Performance Primitive": más allá de un punto, el rendimiento no se puede añadir después, así que diséñalo desde el principio y mantén todo lo demás simple.

Fuentes: charla de Cheney (https://dave.cheney.net/paste/gophercon-uk-2026.html), con código en github.com/pkg/json y github.com/davecheney/bytereader; John Ousterhout, "Define errors out of existence".

Orden de prioridades

  1. Corrección. Nunca la intercambies por velocidad o brevedad.
  2. Forma de la API y big-O. Quién posee la memoria, qué se devuelve, cuánto trabajo hace cada llamada. Esto decide el rendimiento más que cualquier ajuste posterior.
  3. Asignaciones en el camino caliente. Cada asignación cuesta dos veces: una para asignar, otra para el recolector de basura. El recolector puede ser rápido, "pero no asignar siempre será más rápido".
  4. Trabajo por unidad de entrada. Toca cada byte una vez y haz llamadas por token o registro, no por byte.
  5. Microoptimizaciones (inlining manual, trucos de bits, evitar comprobaciones de límites), solo en un camino que un perfil muestre como caliente, y solo con un benchmark que demuestre la ganancia.

Si un cambio no está justificado en el nivel superior, no lo hagas.

Separa caminos calientes y fríos

Decide qué código se ejecuta por evento o por solicitud (caliente) y qué se ejecuta en tiempo de configuración, compilación, inicialización o actualización (frío).

  • Camino frío: escribe el código más simple. Clona en lugar de compartir, usa mapas y fmt, asigna libremente. No lleves aquí modismos del camino caliente; cuestan legibilidad y no compran nada.
  • Camino caliente: puede usar buffers reutilizables, contadores de generación, arrays planos y técnicas similares. Dale a cada técnica un nombre (un tipo pequeño o helper) y un comentario que explique por qué existe, para que el lector no tenga que hacer ingeniería inversa.

Diseña la API para el rendimiento

La API decide cuánto puede ahorrar un llamante. "Estás midiendo el recolector de basura" cuando la API obliga a que cada resultado vaya al heap. Dale forma para que el camino rápido sea el natural; a menudo se puede obtener una ganancia de 2-10x "si estás dispuesto a aceptar una API diferente".

  • No devuelvas valores que deban escapar. Devolver un primitivo a través de un interface{}/any, un string nuevo, un mapa o un puntero a un nuevo struct por elemento fuerza una asignación por llamada (encoding/json.Decoder.Token es el clásico "fábrica de basura"). Devuelve un tipo concreto, o una vista como []byte, y deja que el llamante convierta solo lo que necesita.
  • Devuelve vistas, no copias, cuando el llamante solo necesita leer. Documenta cuánto tiempo es válida la vista ("válida hasta la próxima llamada", como bufio.Scanner.Bytes). Una vista de la entrada sin procesar (los bytes del token, cuyo primer byte da su tipo) es más barata que un valor decodificado que el llamante podría ignorar.
  • Permite que el llamante posea memoria reutilizable. Acepta un argumento de scratch o buffer, o mantenlo en un struct que el llamante conserva, en lugar de asignar por llamada. Permite que los llamantes proporcionen el buffer inicial, para que pueda vivir en la pila o venir de un pool: var buf [8 << 10]byte; d := NewDecoderBuffer(r, buf[:]).
  • Oculta la gestión de buffers detrás de unos pocos métodos. io.Reader copia en un buffer gestionado por el llamante y deja que cada llamante lo rellene. Una API de ventana pequeña mantiene los datos en su lugar: Window() []byte para mirar, Release(n) para consumir, Extend() int para leer más, con "la ventana se invalida con Release o Extend" en el comentario de documentación.
  • Define los errores fuera del bucle caliente. Usa iteración que devuelva un bool o un resultado vacío y reporte el error después: for s.Next() { ... } luego s.Err(). Usa resultados centinela en el camino lento (Extend devuelve 0, luego el llamante pregunta por el error) para que el bucle interno no tenga ramas de error. Trata io.EOF como cualquier otra condición terminal.
  • Cuenta el trabajo por unidad de entrada. Procesa cada byte una vez, haz llamadas a funciones por token o registro en lugar de por byte, y elimina las copias intermedias. Pasar de llamadas por byte a por token suele ser la segunda ganancia más grande después de eliminar asignaciones.
  • Capa la API. Ofrece un bloque de construcción de bajo nivel, sin asignaciones (tokens sin procesar, vistas, iteradores que poseen scratch) y construye la API conveniente sobre él. No hagas que la API conveniente y con asignaciones sea la única.
  • Escribe la propiedad y el tiempo de vida en los comentarios de documentación: qué se toma prestado, qué se retiene, qué es seguro para uso concurrente, qué significa el valor cero.
  • Haz útil el valor cero donde sea natural.
  • Prefiere valores compartidos inmutables y publica nuevas instantáneas, en lugar de locks alrededor de estado mutable compartido, cuando los lectores dominan.

Mide la API, no solo la implementación: compara allocs/op y rendimiento contra la API que estás reemplazando. references/examples.md muestra estas formas una al lado de la otra.

Ajustar un bucle caliente, por último

Solo después de que la API y big-O sean correctos, y un perfil muestre que el bucle es caliente:

  • Un switch sobre un estado se compila en una cadena de comparaciones. Métodos pequeños por estado, elegidos a través de un campo func (d.state = (*Decoder).stateValue), mantienen cada estado legible y pueden eliminar ese despacho.
  • Mueve una comprobación a la función que usa el valor para que el compilador pueda probar que las comprobaciones de límites son innecesarias, y mantén el punto de entrada bajo el presupuesto de inlining (80; una llamada cuesta aproximadamente 52). Verifica con go build -gcflags=-m=2.
  • Fusionar dos funciones para evitar una llamada puede ganar 5-15% en un camino por token, a costa de un anidamiento más profundo. Hazlo solo con un benchmark, y di por qué en un comentario.

Como dice Cheney: no recurras a estos trucos "sin abordar los efectos big O en tu api. No moverá la aguja."

Mantén las funciones pequeñas y planas

Apunta a que cada función haga una cosa, con como máximo una o dos ramas. Como guía, mantén la complejidad ciclomática en o por debajo de 10 y la complejidad cognitiva en o por debajo de 10 (gocyclo, gocognit). Un switch de tipo plano que pasa cada caso a una llamada está bien por encima de eso; el anidamiento no.

  • Nombra condiciones de múltiples términos. Reemplaza if len(a)*len(b) > maxN || size(a)*len(b)+size(b)*len(a) > maxM con if !fitsProduct(a, b). El nombre documenta la intención y la función puede llevar la explicación.
  • Despacha, luego delega. Un switch sobre tipos de nodo o mensaje debe llamar a un método por caso, no contener la lógica de cada caso.
  • Retorna temprano. Cláusulas de guardia primero, el camino principal sin sangría.
  • No repitas un caso. Si dos ramas difieren solo en el orden de los operandos, escribe un helper y llámalo dos veces.
  • Divide preocupaciones mixtas. "Encontrarlo", "construir el nuevo valor" y "ordenar" son tres funciones, no una.
  • Evita parámetros de bandera booleana. Si uno es la opción más clara, dale un nombre significativo y explícalo en el lugar de la llamada.
  • Usa tablas de búsqueda (map[string]Kind, [256]bool) en lugar de cadenas de comparaciones de strings o bytes.

Da forma a los datos para el lector y la CPU

  • Agrupa slices paralelos en un slice de structs. Cuatro slices indexados por el mismo ID son un struct. Se lee mejor y generalmente mejora la localidad.
  • Dale a un mecanismo repetido su propio tipo pequeño. Si la misma lógica de contador-wrap, bitmap o buffer aparece dos veces, es un tipo con dos o tres métodos.
  • Prefiere campos explícitos a bit-packing a menos que se mida que el packing importa. []segment{name, each} supera a un uint64 de flags con aritmética de desplazamiento.
  • Mantén pocas representaciones. Menos estados o tipos significan menos switches duplicados. Fusiona un caso especial en el general cuando no cueste nada medible.

Demuestra afirmaciones de rendimiento

Nunca digas que un cambio es más rápido, más lento o sin asignaciones sin medirlo. Antes de cambiar código sensible al rendimiento:

  1. Guarda una línea base benchstat de los benchmarks relevantes.
  2. Haz el cambio, luego compara. Reporta diferencias significativas con números.
  3. Compara la salida de go build -gcflags=-m con la base para nuevas fugas al heap.
  4. Mantén o añade pruebas testing.AllocsPerRun para caminos que prometan cero asignaciones.
  5. Perfila antes de adivinar la causa de una regresión.

Si simplificar cuesta tiempo medible en el camino caliente y no puedes recuperarlo limpiamente, detente y reporta el compromiso en lugar de añadir silenciosamente complejidad o aceptar la regresión.

Lee references/verify.md para los comandos exactos, incluido cómo medir la complejidad y las trampas que comúnmente causan regresiones. Lee references/examples.md para refactorizaciones antes y después que muestran el estilo.

Sigue las convenciones de Go

  • gofmt, go vet y el linter del proyecto deben pasar. Arregla el código, no debilites ni excluyas reglas del linter; suprime una sola línea solo con una regla nombrada y una razón.
  • Nombres de receptores cortos, consistentes en los métodos de un tipo; no mezcles receptores de puntero y de valor en un tipo sin razón.
  • Los comentarios de documentación comienzan con el nombre del identificador y declaran el contrato.
  • Comenta el por qué, no el qué. Iguala la densidad de comentarios circundante.
  • Biblioteca estándar primero (slices, maps, strings, math/bits) antes de helpers escritos a mano.
  • Nunca edites a mano código generado.

Al revisar o refactorizar

  1. Identifica el camino caliente y sus benchmarks antes de tocar nada.
  2. Mide la complejidad para encontrar las peores funciones; arréglalas primero.
  3. Refactoriza en pasos pequeños, ejecutando pruebas enfocadas después de cada uno.
  4. Mantén el comportamiento idéntico: mismos resultados, mismos presupuestos y fallbacks, mismas garantías de asignación. Cambia el comportamiento solo como una decisión separada y declarada.
  5. Reporta qué cambió, los números de complejidad y benchmark antes/después, y cualquier cosa no verificada.

Skills relacionados