davidvella/go-style

Escreva, revise ou refatore código Go que seja fácil de ler e ainda rápido. Mantém funções pequenas com baixa complexidade ciclomática e cognitiva, coloca desempenho no design da API e na organização dos dados em vez de código engenhoso, e comprova mudanças com benchmarks, contagens de alocações e análise de escape.

O que é go-style?

Esta skill (baseada na palestra 'API Design as a Performance Primitive' de Dave Cheney e nos princípios de John Ousterhout) define um estilo de programação Go que prioriza a legibilidade ao mesmo tempo em que maximiza o desempenho por meio de um design de API consciente e da estruturação de dados. Ela orienta o desenvolvedor a separar caminhos quentes (hot paths) de caminhos frios (cold paths), evitando micro-otimizações prematuras. Para alcançar eficiência, a skill recomenda que a API retorne visões em vez de cópias, permita que o chamador possua memória reutilizável e defina erros fora do loop principal. Além disso, incentiva o uso de buffers fornecidos pelo chamador, funções pequenas e planas (com complexidade ciclomática ≤ 10) e a substituição de slices paralelas por slices de structs para melhor localidade. A validação de desempenho é obrigatória: qualquer alegação de melhoria deve ser acompanhada de benchmarks, análise de escape (go build -gcflags=-m) e testes de alocações. A skill é compatível com ferramentas como Claude Code, Cursor e Codex, e se aplica a qualquer projeto Go que busque equilíbrio entre clareza e velocidade.

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

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Estilo Go: legível e rápido

O objetivo é escrever código que um mantenedor possa ler do início ao fim, com desempenho que vem da forma da API e dos dados, e não de truques espalhados pelo código. Legibilidade e velocidade raramente entram em conflito quando o design está correto; quando entrarem, abra mão da legibilidade apenas em um caminho quente medido e explique isso em um comentário.

Isso segue a abordagem de Dave Cheney, "API Design as a Performance Primitive": além de um certo ponto, o desempenho não pode ser adicionado depois, portanto, projete para ele desde o início e mantenha todo o resto simples.

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

Ordem de prioridades

  1. Corretude. Nunca troque corretude por velocidade ou concisão.
  2. Forma da API e big-O. Quem possui a memória, o que é retornado, quanto trabalho cada chamada realiza. Isso decide o desempenho mais do que qualquer ajuste posterior.
  3. Alocações no caminho quente. Cada alocação custa duas vezes: uma para alocar, outra para o coletor de lixo. O coletor pode ser rápido, "mas não alocar será sempre mais rápido".
  4. Trabalho por unidade de entrada. Toque cada byte uma vez e faça chamadas por token ou registro, não por byte.
  5. Micro-otimizações (inlining manual, truques de bit, evitar verificações de limites), apenas em um caminho que um perfil mostre como quente, e apenas com um benchmark que comprove o ganho.

Se uma mudança não for justificada no nível acima, não a faça.

Separe caminhos quentes e frios

Decida qual código executa por evento ou por requisição (quente) e qual executa em configuração, construção, setup ou atualização (frio).

  • Caminho frio: escreva o código mais simples. Clone em vez de compartilhar, use maps e fmt, aloque à vontade. Não carregue idiomáticas de caminho quente aqui; elas custam legibilidade e não trazem benefício.
  • Caminho quente: pode usar buffers reutilizáveis, contadores de geração, arrays planos e técnicas similares. Dê a cada técnica um nome (um tipo pequeno ou helper) e um comentário explicando por que ela existe, para que o leitor não precise fazer engenharia reversa.

Projete a API para desempenho

A API decide quanto o chamador pode economizar. "Você está fazendo benchmark do coletor de lixo" quando a API força cada resultado para o heap. Molde-a de forma que o caminho rápido seja o natural; um ganho de 2 a 10x está frequentemente disponível "se você estiver disposto a aceitar uma API diferente".

  • Não retorne valores que precisam escapar. Retornar um primitivo através de interface{}/any, uma string nova, um map ou um ponteiro para um novo struct por item força uma alocação por chamada (encoding/json.Decoder.Token é o clássico "fábrica de lixo"). Retorne um tipo concreto, ou uma visão como []byte, e deixe o chamador converter apenas o que precisar.
  • Retorne visões, não cópias, quando o chamador só precisa ler. Documente por quanto tempo a visão é válida ("válida até a próxima chamada", como bufio.Scanner.Bytes). Uma visão da entrada bruta (os bytes do token, cujo primeiro byte indica seu tipo) é mais barata do que um valor decodificado que o chamador pode ignorar.
  • Deixe o chamador possuir memória reutilizável. Aceite um argumento de rascunho ou buffer, ou mantenha-o em um struct que o chamador conserva, em vez de alocar por chamada. Permita que os chamadores forneçam o buffer inicial, para que ele possa viver na pilha ou vir de um pool: var buf [8 << 10]byte; d := NewDecoderBuffer(r, buf[:]).
  • Esconda o gerenciamento de buffer por trás de alguns métodos. io.Reader copia para um buffer gerenciado pelo chamador e deixa o reabastecimento para cada chamador. Uma API de janela pequena mantém os dados no lugar: Window() []byte para olhar, Release(n) para consumir, Extend() int para ler mais, com "a janela é invalidada por Release ou Extend" no comentário de documentação.
  • Defina erros para fora do loop quente. Use iteração que retorna um bool ou um resultado vazio e relata o erro depois: for s.Next() { ... } então s.Err(). Use resultados sentinela no caminho lento (Extend retorna 0, então o chamador pergunta pelo erro) para que o loop interno não tenha ramificações de erro. Trate io.EOF como qualquer outra condição terminal.
  • Conte trabalho por unidade de entrada. Processe cada byte uma vez, faça chamadas de função por token ou registro em vez de por byte e remova cópias intermediárias. Passar de chamadas por byte para chamadas por token é frequentemente o segundo maior ganho depois de remover alocações.
  • Camadas na API. Ofereça um bloco de construção de baixo nível, sem alocações (tokens brutos, visões, iteradores que possuem rascunhos) e construa a API conveniente sobre ele. Não torne a API conveniente e alocadora a única.
  • Escreva propriedade e tempo de vida nos comentários de documentação: o que é emprestado, o que é retido, o que é seguro para uso concorrente, o que o valor zero significa.
  • Torne o valor zero útil onde for natural.
  • Prefira valores compartilhados imutáveis e publique novos instantâneos, em vez de locks em torno de estado mutável compartilhado, quando leitores dominarem.

Meça a API, não apenas a implementação: compare allocs/op e throughput contra a API que você está substituindo. references/examples.md mostra essas formas lado a lado.

Ajustando um loop quente, por último

Somente depois que a API e o big-O estiverem corretos e um perfil mostrar que o loop é quente:

  • Um switch sobre um estado compila para uma cadeia de comparações. Métodos pequenos por estado, escolhidos através de um campo func (d.state = (*Decoder).stateValue), mantêm cada estado legível e podem remover esse dispatch.
  • Mova uma verificação para dentro da função que usa o valor para que o compilador possa provar que as verificações de limites são desnecessárias, e mantenha o ponto de entrada dentro do orçamento de inlining (80; uma chamada custa cerca de 52). Verifique com go build -gcflags=-m=2.
  • Mesclar duas funções para evitar uma chamada pode ganhar 5-15% em um caminho por token, ao custo de aninhamento mais profundo. Faça isso apenas com um benchmark e explique o porquê em um comentário.

Como Cheney diz: não recorra a esses truques "sem abordar os efeitos de big O em sua API. Isso não moverá o ponteiro."

Mantenha funções pequenas e planas

Busque que cada função faça uma coisa, com no máximo um ou dois ramos. Como guia, mantenha a complexidade ciclomática em ou abaixo de 10 e a complexidade cognitiva em ou abaixo de 10 (gocyclo, gocognit). Um switch de tipo plano que passa cada caso para uma chamada é aceitável acima disso; aninhamento não é.

  • Nomeie condições de múltiplos termos. Substitua if len(a)*len(b) > maxN || size(a)*len(b)+size(b)*len(a) > maxM por if !fitsProduct(a, b). O nome documenta a intenção e a função pode carregar a explicação.
  • Despache, depois delegue. Um switch sobre tipos de nó ou mensagem deve chamar um método por caso, não conter a lógica de cada caso.
  • Retorne cedo. Clausulas de guarda primeiro, o caminho principal sem indentação.
  • Não repita um caso. Se dois ramos diferem apenas na ordem dos operandos, escreva um helper e chame-o duas vezes.
  • Divida preocupações misturadas. "Encontrar", "construir o novo valor" e "organizar" são três funções, não uma.
  • Evite parâmetros booleanos como bandeiras. Se um for a opção mais clara, dê a ele um nome significativo e explique-o no local da chamada.
  • Use tabelas de consulta (map[string]Kind, [256]bool) em vez de cadeias de comparações de strings ou bytes.

Molde dados para o leitor e para a CPU

  • Agrupe slices paralelas em um slice de structs. Quatro slices indexadas pelo mesmo ID são um struct. Isso lê melhor e geralmente melhora a localidade.
  • Dê a um mecanismo repetido seu próprio tipo pequeno. Se a mesma lógica de contador-enrolador, bitmap ou buffer aparecer duas vezes, é um tipo com dois ou três métodos.
  • Prefira campos explícitos em vez de compactação de bits a menos que a compactação seja medida como relevante. []segment{name, each} supera um uint64 de flags com aritmética de deslocamento.
  • Mantenha representações poucas. Menos estados ou tipos significam menos switchs duplicados. Mescle um caso especial no geral quando isso não custar nada mensurável.

Prove alegações de desempenho

Nunca afirme que uma mudança é mais rápida, mais lenta ou livre de alocações sem medi-la. Antes de alterar código sensível a desempenho:

  1. Salve uma linha de base benchstat dos benchmarks relevantes.
  2. Faça a mudança e compare. Reporte diferenças significativas com números.
  3. Diferencie a saída de go build -gcflags=-m contra a base para novas fugas de heap.
  4. Mantenha ou adicione testes testing.AllocsPerRun para caminhos que prometem zero alocações.
  5. Faça perfil antes de adivinhar a causa de uma regressão.

Se simplificar custar tempo mensurável no caminho quente e você não puder recuperá-lo de forma limpa, pare e reporte a troca em vez de adicionar silenciosamente complexidade de volta ou aceitar a regressão.

Leia references/verify.md para os comandos exatos, incluindo como medir complexidade e as armadilhas que comumente causam regressões. Leia references/examples.md para refatorações antes e depois que mostram o estilo.

Siga as convenções Go

  • gofmt, go vet e o linter do projeto devem passar. Corrija o código, não enfraqueça ou exclua regras do linter; suprima uma única linha apenas com uma regra nomeada e motivo.
  • Nomes de receptores curtos, consistentes entre os métodos de um tipo; não misture receptores de ponteiro e valor em um tipo sem motivo.
  • Comentários de documentação começam com o nome do identificador e declaram o contrato.
  • Comente o porquê, não o quê. Combine a densidade de comentários ao redor.
  • Biblioteca padrão primeiro (slices, maps, strings, math/bits) antes de helpers escritos à mão.
  • Nunca edite manualmente código gerado.

Ao revisar ou refatorar

  1. Identifique o caminho quente e seus benchmarks antes de tocar em qualquer coisa.
  2. Meça a complexidade para encontrar as piores funções; corrija essas primeiro.
  3. Refatore em pequenos passos, executando testes focados após cada um.
  4. Mantenha o comportamento idêntico: mesmos resultados, mesmos orçamentos e fallbacks, mesmas garantias de alocação. Mude o comportamento apenas como uma decisão separada e declarada.
  5. Reporte o que mudou, os números de complexidade antes/depois e os benchmarks, e qualquer coisa não verificada.

Habilidades Relacionadas