Em Go, o comentário também é código

2026-04-08

post-thumb

Índice

Comentário é aquela coisa que todo mundo aprende no primeiro dia de qualquer linguagem e já assume saber tudo sobre. Em Go, essa suposição custa caro. A linguagem fez do comentário uma ferramenta de primeira classe: um mesmo // pode ser só um lembrete pra você mesma, virar documentação pública, decidir o que o compilador vai incluir no binário, gerar código novo em tempo de build ou até virar código C de verdade.

Esse post é sobre os comentários em Go que fazem mais do que comentar.


A sintaxe do básico

Pra tirar isso do caminho: Go suporta dois tipos de comentário, exatamente como C.

// comentário de linha única
// que pode ocupar várias linhas coladas

/* comentário de bloco
   que funciona bem pra blocos longos
   ou quando você precisa de um comentário no meio de uma expressão */

x := /* inline no meio da linha */ 42

Na prática, // é o padrão em quase todo código Go idiomático, incluindo a biblioteca padrão. O /* */ fica reservado pra casos específicos, tipo documentação de pacote muito longa ou comentário inline pontual.

Até aqui nada mágico. A mágica começa quando você entende que o compilador e as ferramentas do Go leem alguns desses comentários.


1. Documentação que vira site público: godoc

A primeira regra não escrita do Go é: o comentário que precede uma declaração vira a documentação daquela declaração. Sem anotação, sem tag, sem nada. Basta colocar o comentário imediatamente acima, sem linha em branco no meio.

// Foo aplica a transformação foo na string recebida.
// Retorna erro se a string não puder ser foo'ada.
func Foo(s string) error {
    // ...
}

Esse comentário não é só pra quem lê o código no editor. Ele alimenta o pkg.go.dev, o go doc no terminal e qualquer ferramenta que gere documentação de pacote Go. Exportou um símbolo (nome começando com maiúscula)? A convenção é que ele venha com um comentário descritivo, começando com o próprio nome do símbolo.

Funciona pra tudo que é exportável:

package objetos

// Objeto é um container genérico de alguma coisa.
type Objeto struct{}

// Processar aplica a lógica de processamento no Objeto.
// Retorna erro se o objeto estiver em estado inválido.
func (o Objeto) Processar() error {
    return nil
}

// Lista contém todos os objetos registrados atualmente.
var Lista []Objeto

// MaxObjetos define o limite máximo de objetos permitidos.
const MaxObjetos = 50

Também existe a documentação de pacote: um comentário colocado imediatamente antes da declaração package, no início de qualquer arquivo do pacote. Convenção é colocar isso em um arquivo chamado doc.go quando a descrição for longa.

// Package objetos fornece um container genérico e seguro pra uso concorrente,
// com registro global e limites configuráveis.
package objetos

Uma sutileza importante: a primeira frase do comentário é o que aparece no índice do pkg.go.dev. Escreva a primeira frase pensando nela como um resumo de uma linha. O resto do parágrafo aparece só quando alguém abre a página completa.


2. Comentários que controlam o que compila: build constraints

Aqui a coisa começa a ficar divertida. Go tem um sistema de build constraints (também chamado de build tags) que permite dizer pro compilador: “esse arquivo só faz sentido em Linux”, ou “só compila quando a flag integration estiver ativa”. E tudo isso vive dentro de um comentário.

A sintaxe clássica (pré Go 1.17):

// +build linux

package main

// tudo aqui só compila em Linux

A regra é bem específica: o comentário // +build ... tem que estar nas primeiras linhas do arquivo, antes da declaração package, seguido por uma linha em branco antes do package. Se você esquecer a linha em branco, vira comentário normal e o compilador ignora.

A partir do Go 1.17 existe uma sintaxe nova, mais limpa:

//go:build linux

package main

As duas formas funcionam, e projetos modernos geralmente incluem ambas pra compatibilidade.

Operadores

Os build constraints aceitam lógica booleana:

OperadorSintaxe clássicaSintaxe novaSignificado
OR// +build linux darwin//go:build linux || darwincompila em Linux ou macOS
AND// +build windows,386//go:build windows && 386compila em Windows e 386
NOT// +build !windows//go:build !windowscompila em qualquer coisa menos Windows

Na sintaxe clássica, múltiplas linhas somam como AND:

// +build windows
// +build 386

package main

Isso é equivalente a windows AND 386.

Tags customizadas

Você não precisa se limitar a sistemas operacionais e arquiteturas. Qualquer palavra vira uma tag, e você ativa via go build -tags="minhatag".

//go:build integration

package repositorio

// testes que só rodam com: go test -tags=integration ./...

É assim que bibliotecas separam testes unitários de testes que precisam de banco de dados, por exemplo.

A tag ignore

Uma convenção útil: usar ignore como tag pra arquivos que você quer manter no repositório mas que o compilador não deve incluir em nenhuma build normal.

//go:build ignore

package main

// esse arquivo é um utilitário de geração manual
// rode com: go run arquivo.go

Nome de arquivo como build constraint implícito

Bônus: se um arquivo se chama foo_linux.go, foo_windows.go ou foo_amd64.go, o Go aplica a constraint automaticamente, sem precisar do comentário. O sufixo é o próprio build tag. Combinações funcionam: foo_linux_amd64.go só compila em Linux amd64.


3. Comentários que geram código: go generate

Esse é o que mais surpreende gente que vem de outras linguagens. Go tem um subcomando chamado go generate que varre seu código procurando comentários especiais e roda os comandos que estão dentro deles. A sintaxe é:

//go:generate <comando> <argumentos>

Atenção ao detalhe: não pode ter espaço entre as duas barras e o go:generate. // go:generate não funciona. Tem que ser //go:generate, tudo colado.

O exemplo canônico é a ferramenta stringer, que gera automaticamente um método String() pra tipos enumerados. Partindo desse código:

package pedido

//go:generate stringer -type=Status

type Status int

const (
    StatusPendente Status = iota
    StatusPago
    StatusCancelado
    StatusEntregue
)

Quando você roda go generate ./..., o Go encontra aquele comentário, executa stringer -type=Status no diretório e gera um arquivo status_string.go com algo assim:

// Code generated by "stringer -type=Status"; DO NOT EDIT.

package pedido

func (i Status) String() string {
    switch i {
    case StatusPendente:
        return "Pendente"
    case StatusPago:
        return "Pago"
    case StatusCancelado:
        return "Cancelado"
    case StatusEntregue:
        return "Entregue"
    }
    return "Status desconhecido"
}

A partir daí fmt.Println(StatusPago) imprime Pago, não o inteiro subjacente. Tudo sem você escrever uma linha do switch.

O go generate não roda automaticamente durante go build. Ele é um comando explícito que você deve rodar (idealmente automatizado via Makefile ou CI) antes de compilar. É intencional: o Go prefere que o código gerado seja commitado no repositório, não produzido em build time.

Você pode usar go:generate com qualquer comando, não só stringer. Ferramentas comuns incluem mockgen (geração de mocks), protoc-gen-go (Protobuf), sqlc (queries SQL tipadas) e afins.


4. Comentários que viram código C: Cgo

Aqui a mágica sai de Go e entra em território estrangeiro. Cgo é o mecanismo oficial pra chamar código C a partir de Go, e a forma de escrever esse código C é… você adivinhou, dentro de comentários.

O padrão é colocar um bloco de comentário imediatamente antes do import "C" (sem linha em branco entre os dois). Tudo que estiver nesse comentário é passado literalmente pro compilador C.

package main

// #include <stdio.h>
// #include <errno.h>
import "C"

Você também pode escrever funções C inteiras ali dentro:

package main

// #include <stdio.h>
//
// static void imprimir(char *s) {
//     printf("%s\n", s);
// }
import "C"

func main() {
    C.imprimir(C.CString("oi, mundo C"))
}

Esse imprimir é uma função C real, compilada pelo compilador C do sistema, e invocada do Go como se fosse uma função Go qualquer.

Diretivas #cgo

Além do código C puro, tem umas diretivas especiais começando com #cgo que configuram flags do compilador:

package imagens

// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #cgo pkg-config: png cairo
// #include <png.h>
import "C"

Traduzindo cada linha:

  • CFLAGS: -DPNG_DEBUG=1 passa essa flag pro compilador C em todas as arquiteturas
  • amd64 386 CFLAGS: -DX86=1 passa essa flag só em amd64 e 386
  • LDFLAGS: -lpng diz pro linker que precisa linkar com libpng
  • pkg-config: png cairo usa o pkg-config pra descobrir automaticamente os flags corretos pra libpng e libcairo

Cgo é poderoso, mas tem um custo real de performance (cada chamada Go para C cruza uma fronteira) e complica bastante o cross-compile. Eu uso em último caso, quando não existe biblioteca Go nativa pro que eu preciso.


Por que isso importa

Dá pra escrever Go ignorando tudo isso. Você consegue viver anos usando // só como comentário normal e nunca esbarrar num //go:build. Mas entender esse mecanismo muda duas coisas na sua relação com a linguagem.

Primeiro, você passa a ler a biblioteca padrão de um jeito diferente. De repente aquele arquivo com //go:build darwin no topo faz sentido, e você entende por que o pacote net tem comportamentos sutilmente diferentes em cada sistema operacional. A biblioteca padrão é um curso prático de como usar build constraints bem.

Segundo, e mais importante: você percebe que Go tomou uma decisão de design deliberada e incomum. Enquanto outras linguagens inventam anotações (@override, @Component), atributos ([Serializable]), macros (#[derive]) ou decorators (@classmethod), Go reaproveitou uma coisa que toda linguagem já tem: o comentário. A sintaxe continua simples. Quem não conhece o mecanismo ignora sem perder nada. Quem conhece, ganha superpoderes sem poluir a gramática da linguagem.

É o tipo de decisão que parece simples demais pra funcionar, e depois de um tempo usando você entende por que funciona.


Pra levar no bolso

Um resumo pra deixar aberto enquanto você escreve:

ComentárioPra que serveQuem processa
// Foo faz tal coisa. acima de uma declaraçãoVira documentação públicago doc, pkg.go.dev
// Package x faz Y. antes de packageDocumentação do pacote inteirogo doc, pkg.go.dev
//go:build linux (ou // +build linux)Restringe compilação a Linuxcompilador
//go:build integrationCompila só com -tags=integrationcompilador
//go:build ignoreArquivo fica no repo mas não compilacompilador
//go:generate cmd argsRoda cmd args no go generatego generate
Comentário antes de import "C"Vira código C literalcompilador C via cgo
// #cgo LDFLAGS: -lpngConfigura flags do cgocgo

O próximo passo é olhar o código-fonte de um pacote grande da biblioteca padrão (recomendo o runtime ou o net) e procurar esses comentários. Você vai encontrar usos criativos que não cabem num post só.

E da próxima vez que alguém te disser que comentário não é código, você sorri e manda a pessoa abrir um arquivo .go.