Documentação com godoc e boas práticas de comentário

Documentação com godoc e boas práticas de comentário

Documentação em Go: comentários que começam pelo nome, cabeçalhos, listas e blocos que o gofmt reformata, exemplos testáveis que precisam compilar, go doc com -all e -u, go doc -http, pkg.go.dev e a licença, Deprecated e a regra exported do revive no CI.
Golang

• • 18 min de leitura

Em Go, documentação não é um arquivo separado nem uma ferramenta externa — é o próprio código comentado. O godoc extrai comentários diretamente do código fonte e os transforma em documentação navegável, disponível tanto na linha de comando quanto no navegador.

Essa abordagem tem uma consequência poderosa: documentação que não está no código não existe para o godoc. Isso incentiva manter a documentação próxima do que ela descreve — reduzindo a probabilidade de ficar desatualizada.

As regras do godoc

O godoc segue convenções simples e estritas. Comentários de documentação são aqueles imediatamente antes de uma declaração, sem linha em branco entre o comentário e o identificador:

// Package financeiro fornece tipos e operações para gestão financeira,
// incluindo contas bancárias, transferências e relatórios de movimentação.
//
// Uso básico:
//
//  conta, err := financeiro.NovaConta("Ricardo", 1000.00)
//  if err != nil {
//      log.Fatal(err)
//  }
//  conta.Depositar(500.00)
//
// Para mais exemplos, consulte os arquivos _test.go do pacote.
package financeiro

Regra 1: o comentário começa com o nome do identificador. Isso permite que o godoc gere índices e o texto faça sentido quando lido isoladamente.

Regra 2: sem linha em branco entre o comentário e a declaração. Uma linha em branco quebra a associação — o godoc não vai reconhecer o comentário como documentação daquele identificador.

Regra 3: use frases completas. O primeiro parágrafo é exibido nos índices como sumário.

Documentando funções e métodos

// NovaConta cria e retorna uma nova conta bancária para o titular informado.
// O depósito inicial deve ser maior ou igual a zero; valores negativos retornam erro.
//
// O titular é obrigatório — uma string vazia retorna ErrTitularVazio.
//
// Exemplo:
//
//  conta, err := NovaConta("Ana Silva", 500.00)
//  if err != nil {
//      // tratar erro
//  }
func NovaConta(titular string, depositoInicial float64) (*Conta, error) {
    if titular == "" {
        return nil, ErrTitularVazio
    }
    if depositoInicial < 0 {
        return nil, fmt.Errorf("depósito inicial não pode ser negativo: %.2f", depositoInicial)
    }
    return &Conta{
        titular: titular,
        saldo:   depositoInicial,
    }, nil
}

// Depositar adiciona o valor informado ao saldo da conta.
// Retorna ErrValorInvalido se o valor for menor ou igual a zero.
func (c *Conta) Depositar(valor float64) error {
    if valor <= 0 {
        return ErrValorInvalido
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    c.saldo += valor
    return nil
}

// Saldo retorna o saldo atual da conta.
// Esta operação é thread-safe.
func (c *Conta) Saldo() float64 {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.saldo
}

// Titular retorna o nome do titular da conta.
func (c *Conta) Titular() string {
    return c.titular
}

// Sacar retira o valor informado do saldo da conta.
// Retorna ErrValorInvalido se o valor for menor ou igual a zero e
// ErrSaldoInsuficiente se o valor for maior que o saldo.
func (c *Conta) Sacar(valor float64) error {
    if valor <= 0 {
        return ErrValorInvalido
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if valor > c.saldo {
        return ErrSaldoInsuficiente
    }
    c.saldo -= valor
    return nil
}

Documentando tipos e constantes

// Conta representa uma conta bancária com operações de depósito, saque e transferência.
// Conta é segura para uso concorrente.
type Conta struct {
    titular string
    saldo   float64
    mu      sync.RWMutex
}

// StatusConta representa o estado atual de uma conta bancária.
type StatusConta int

const (
    // StatusAtiva indica que a conta está ativa e operacional.
    StatusAtiva StatusConta = iota

    // StatusBloqueada indica que a conta foi bloqueada temporariamente.
    // Operações de débito são recusadas enquanto a conta estiver bloqueada.
    StatusBloqueada

    // StatusEncerrada indica que a conta foi encerrada definitivamente.
    // Nenhuma operação é permitida em contas encerradas.
    StatusEncerrada
)

// ErrSaldoInsuficiente é retornado quando um saque ou transferência
// excede o saldo disponível na conta.
var ErrSaldoInsuficiente = errors.New("saldo insuficiente")

// ErrTitularVazio é retornado quando o nome do titular é uma string vazia.
var ErrTitularVazio = errors.New("titular não pode ser vazio")

// ErrValorInvalido é retornado quando o valor de uma operação é menor
// ou igual a zero.
var ErrValorInvalido = errors.New("valor deve ser maior que zero")

Formatação especial no godoc

O godoc reconhece alguns padrões de formatação automaticamente:

// Processar executa o pipeline de processamento em três fases:
//
//  1. Validação dos dados de entrada
//  2. Transformação e enriquecimento
//  3. Persistência no banco de dados
//
// Considerações de performance:
//
// A fase de transformação pode ser custosa para entradas grandes.
// Para lotes acima de 10.000 itens, considere usar ProcessarEmLotes.
//
// Exemplos de uso típicos:
//
//  // Processamento simples
//  err := Processar(ctx, dados)
//
//  // Com opções
//  err := Processar(ctx, dados, ComParalelismo(4), ComTimeout(30*time.Second))
//
// Código de exemplo completo em https://pkg.go.dev/meu-pacote#example-Processar
func Processar(ctx context.Context, dados []byte, opts ...Opcao) error {

Blocos de código são linhas indentadas em relação ao texto do comentário, e são exibidos como código pré-formatado. Desde o Go 1.19, o gofmt também formata os comentários de documentação: um bloco indentado com dois espaços, como nos exemplos deste artigo, é reescrito com um tab depois do //.

Listas numeradas com o padrão N. são reconhecidas automaticamente desde o Go 1.19.

Links — URLs completas são automaticamente transformadas em hiperlinks.

Exemplos executáveis

Funções de exemplo são verificadas pelo go test e exibidas na documentação:

// arquivo: financeiro/conta_example_test.go
package financeiro_test

import (
    "fmt"
    "log"

    "meu-modulo/financeiro"
)

func ExampleNovaConta() {
    conta, err := financeiro.NovaConta("Ricardo Matos", 1000.00)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Titular: %s\n", conta.Titular())
    fmt.Printf("Saldo: R$ %.2f\n", conta.Saldo())
    // Output:
    // Titular: Ricardo Matos
    // Saldo: R$ 1000.00
}

func ExampleConta_Depositar() {
    conta, _ := financeiro.NovaConta("Ana", 500.00)

    if err := conta.Depositar(250.00); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Saldo após depósito: R$ %.2f\n", conta.Saldo())
    // Output:
    // Saldo após depósito: R$ 750.00
}

func ExampleConta_Sacar_saldoInsuficiente() {
    conta, _ := financeiro.NovaConta("Carlos", 100.00)

    err := conta.Sacar(200.00)
    fmt.Println(err)
    // Output:
    // saldo insuficiente
}

A convenção de nomenclatura para exemplos: - ExampleFuncao — exemplo para uma função - ExampleTipo_Metodo — exemplo para um método - ExampleTipo_Metodo_sufixo — múltiplos exemplos para o mesmo método

Rodando o godoc localmente

# Instalar godoc
go install golang.org/x/tools/cmd/godoc@latest

# Iniciar servidor local na porta 6060
godoc -http=:6060

Acesse http://localhost:6060/pkg/meu-modulo/ para ver a documentação do seu módulo renderizada. Nas versões recentes do Go, não é preciso instalar nada: go doc -http, rodado dentro do módulo, abre um servidor com a documentação do pacote atual.

go doc: documentação na linha de comando

# Documentação de um pacote
go doc fmt

# Documentação de uma função específica
go doc fmt.Printf

# Documentação de um tipo
go doc net/http.Request

# Documentação de um campo (métodos usam a mesma forma: Tipo.Metodo)
go doc net/http.Request.Header

# Documentação completa do pacote: tudo o que é exportado
go doc -all ./financeiro/

# Incluindo os identificadores não exportados
go doc -u -all ./financeiro/

# Código-fonte de uma função
go doc -src fmt.Printf

pkg.go.dev: documentação pública

Todo módulo publicado no GitHub (ou qualquer VCS público) aparece automaticamente em pkg.go.dev após ser referenciado por um go get. A documentação é gerada automaticamente a partir dos comentários do código — desde que o repositório tenha uma licença que o site reconheça. Sem ela, a página do módulo aparece, mas a documentação fica oculta, conforme a política de licenças do pkg.go.dev.

Para garantir boa apresentação no pkg.go.dev:

// Package meuservico fornece um cliente HTTP para a API de Pagamentos XYZ.
//
// # Instalação
//
//  go get github.com/minha-empresa/meuservico
//
// # Uso rápido
//
//  cliente := meuservico.NovoCliente("minha-api-key")
//  resposta, err := cliente.CriarPagamento(ctx, meuservico.Pagamento{
//      Valor:    100.00,
//      Moeda:    "BRL",
//      Descricao: "Pedido #1001",
//  })
//
// # Autenticação
//
// Todas as requisições exigem uma API key válida, obtida no painel em
// https://app.meuservico.com/api-keys
//
// # Tratamento de erros
//
// Erros da API são retornados como *meuservico.ErroAPI, que contém o
// código HTTP e a mensagem de erro detalhada.
package meuservico

Os cabeçalhos # Seção são suportados desde o Go 1.19: o pacote go/doc/comment, que as ferramentas de documentação usam, os converte em <h3>. Para ser reconhecida como cabeçalho, a linha precisa estar sozinha, sem indentação e entre linhas em branco; colada ao parágrafo seguinte, ela vira texto comum, com o # à mostra.

README.md e doc.go

Para pacotes com documentação extensa, é comum separar o comentário do pacote em um arquivo dedicado doc.go:

// doc.go — contém apenas o comentário do pacote e a declaração package

// Package cache implementa uma camada de cache em memória com suporte a
// expiração por TTL, eviction por LRU e métricas de hit/miss.
//
// # Exemplo básico
//
//  c := cache.Novo(cache.Config{
//      Capacidade: 1000,
//      TTL:        5 * time.Minute,
//  })
//
//  c.Set("chave", "valor")
//
//  v, existe := c.Get("chave")
//  if existe {
//      fmt.Println(v)
//  }
//
// # Eviction policy
//
// Quando a capacidade máxima é atingida, o item menos recentemente usado
// (LRU — Least Recently Used) é removido automaticamente.
package cache

Convenções de comentário por tipo

// Comentários de pacote: "Package nome fornece/implementa..."
package autenticacao

// Tipos: "NomeTipo representa/é um/a..."
// Usuario representa um usuário autenticado no sistema.
type Usuario struct { }

// Interfaces: "NomeInterface é implementado por..."
// Autenticador é implementado por qualquer tipo que possa validar credenciais.
type Autenticador interface { }

// Funções construtoras: "NovoTipo cria/retorna..."
// NovoUsuario cria e retorna um novo usuário com o email e senha informados.
func NovoUsuario(email, senha string) (*Usuario, error) { }

// Métodos de leitura: "Nome retorna..."
// Email retorna o endereço de email do usuário.
func (u *Usuario) Email() string { }

// Métodos de escrita: "Nome define/atualiza/registra..."
// DefinirSenha atualiza a senha do usuário com o hash fornecido.
func (u *Usuario) DefinirSenha(hash string) { }

// Métodos booleanos: "Nome relata/verifica se..."
// EstaAtivo relata se o usuário está ativo no sistema.
func (u *Usuario) EstaAtivo() bool { }

// Erros sentinela: "Err... é retornado quando..."
// ErrCredenciaisInvalidas é retornado quando email ou senha não conferem.
var ErrCredenciaisInvalidas = errors.New("credenciais inválidas")

// Constantes: explicar o valor quando não é óbvio
const (
    // SessaoMaxima define o tempo máximo de uma sessão inativa antes da expiração.
    SessaoMaxima = 30 * time.Minute

    // TentativasMaximas define o número de tentativas de login antes do bloqueio.
    TentativasMaximas = 5
)

Deprecated: marcando APIs obsoletas

// Conectar estabelece uma conexão com o servidor.
//
// Deprecated: use ConectarComContext para suporte a cancelamento e timeout.
// Esta função será removida na versão 3.0.
func Conectar(host string) (*Conexao, error) {
    return ConectarComContext(context.Background(), host)
}

// ConectarComContext estabelece uma conexão respeitando o context fornecido,
// permitindo cancelamento e timeout.
func ConectarComContext(ctx context.Context, host string) (*Conexao, error) {
    // implementação atual
}

Ferramentas como staticcheck detectam uso de funções marcadas como Deprecated: e emitem avisos.

Documentação de erros esperados

// Transferir move o valor especificado da conta de origem para a de destino.
//
// Erros possíveis:
//   - ErrSaldoInsuficiente: saldo da conta de origem é menor que o valor
//   - ErrContaBloqueada: conta de origem ou destino está bloqueada
//   - ErrValorInvalido: valor é menor ou igual a zero
//   - ErrMesmasConta: origem e destino são a mesma conta
func (s *Servico) Transferir(ctx context.Context, origemID, destinoID string, valor float64) error {

Checklist de documentação

Antes de publicar ou fazer code review, verifique:

□ O pacote tem comentário começando com "Package nome..."?
□ Toda função/método exportado tem comentário começando com seu nome?
□ Todo tipo exportado tem comentário descritivo?
□ Erros sentinela exportados têm comentário explicando quando são retornados?
□ Funções complexas têm exemplos executáveis?
□ Parâmetros com comportamento não óbvio estão documentados?
□ APIs obsoletas estão marcadas com "Deprecated:"?
□ O comentário faz sentido quando lido isoladamente do código?
□ O revive (regra exported, no golangci-lint) não aponta símbolo público sem comentário?

Em Go, a documentação mora no código: um comentário logo acima da declaração, começando pelo nome do identificador, vira a página do pacote no go doc e no pkg.go.dev. Os exemplos testáveis são o recurso que mais rende, porque ilustram o uso e falham no go test quando a saída muda, mas só funcionam se o código que eles chamam existir de fato. A formatação dos comentários é simples, com cabeçalhos #, listas, blocos indentados e links, e o gofmt a mantém uniforme. Para que nada exportado fique sem comentário, a verificação deve ser automática, com a regra exported do revive no golangci-lint, e não um item de checklist lembrado na revisão.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Na primeira versão deste artigo, os exemplos chamavam conta.Titular() e conta.Sacar(200.00), mas o pacote não tinha esses métodos. O autor rodou go test ./... para conferir. O que aconteceu com os três exemplos, inclusive os que só usavam métodos existentes?

Ver resposta

✓ Resposta: Exemplos ficam em arquivos _test.go e são compilados junto com o pacote de teste, como qualquer teste. Um único método inexistente impede a compilação do pacote de teste inteiro, e nenhum exemplo roda, nem os corretos. Num teste com o código da primeira versão, o go vet parou em conta.Titular undefined (type *financeiro.Conta has no field or method Titular, but does have unexported field titular); com os dois métodos acrescentados, os três exemplos passaram. É justamente essa a vantagem dos exemplos testáveis sobre um trecho de código num comentário: um exemplo que deixa de compilar ou cuja saída muda faz o go test falhar, enquanto um comentário desatualizado continua lá, errado, sem que ninguém perceba. Vale lembrar que só os exemplos com o comentário // Output: são executados; sem ele, o exemplo é apenas compilado.

Exercício 2

Um desenvolvedor quer revisar as funções auxiliares internas de um pacote e roda go doc -all ./financeiro/, esperando ver tudo. As funções não exportadas não aparecem. Por quê, e qual é o comando?

Ver resposta

✓ Resposta: O -all mostra a documentação completa do pacote, mas só do que é exportado, que é o que um usuário do pacote pode chamar. Para incluir os identificadores não exportados, a flag é -u. Num teste com uma função ajudante, minúscula, o go doc -all não a listou, e o go doc -u -all mostrou func ajudante(). Os dois combinados dão a visão completa, útil para quem mantém o pacote. Outra forma de consultar a documentação é go doc -http, das versões recentes, que abre um servidor com as páginas do pacote atual. Para o código interno de um pacote grande, porém, o caminho mais comum ainda é o editor: o gopls mostra a documentação de qualquer identificador, exportado ou não, ao passar o mouse sobre ele.

Exercício 3

No pkg.go.dev, a seção # Uso rápido de um pacote aparece como texto comum, com o # visível, enquanto # Instalação, logo acima, virou um cabeçalho. No código, as duas linhas parecem iguais. O que diferencia uma da outra?

Ver resposta

✓ Resposta: Um cabeçalho de comentário precisa ser um parágrafo de uma linha só: começar com # e um espaço, sem indentação, e ter linhas em branco antes e depois. Se a linha seguinte tiver texto, as duas formam um parágrafo comum, e o # vira um caractere qualquer. Num teste com o pacote go/doc/comment, que as ferramentas usam, Instalação. entre linhas em branco virou <h3>, mesmo com o ponto final, e # Uso rápido seguido de uma linha de texto saiu dentro de um <p>. A correção é só a linha // vazia depois do cabeçalho. Como a diferença é visual e não gera aviso, conferir o resultado com go doc ou go doc -http antes de publicar evita descobrir o problema na página pública.

Exercício 4

Depois de atualizar o Go e rodar gofmt -w ., um PR que deveria mudar uma linha aparece com dezenas de comentários alterados, em que os blocos de exemplo trocaram dois espaços por um tab depois do //. O código não mudou. O que provocou isso, e como evitar o ruído?

Ver resposta

✓ Resposta: Desde o Go 1.19, o gofmt formata também os comentários de documentação: blocos de código passam a ser indentados com um tab, listas ganham uma indentação padrão, e linhas em branco excedentes são removidas. Num teste com os comentários deste artigo, todo bloco indentado com dois espaços, como // conta, err := ..., foi reescrito como //<tab>conta, err := .... O conteúdo é o mesmo, mas o diff mostra cada linha. O ruído aparece uma vez só, na primeira formatação depois da atualização. O caminho é fazer essa reformatação num commit separado, sem nenhuma outra mudança, para que o PR funcional continue legível; e manter o gofmt no CI, para que o código nunca mais fique fora do padrão a ponto de gerar um diff desses.

Exercício 5

Um checklist de revisão pedia para conferir que "go doc ./... não exibe missing documentation". O revisor roda o comando e recebe um erro. Qual é o erro, e qual ferramenta faz essa verificação?

Ver resposta

✓ Resposta: O go doc mostra a documentação de um pacote ou símbolo por vez e não aceita padrões com .... Num teste, o comando terminou com does not specify a single package. Ele também nunca reclama de símbolos sem comentário: apenas mostra a declaração sem texto. Quem faz essa verificação é a regra exported do revive, disponível no golangci-lint. Num teste com uma função exportada sem comentário, ele apontou exported function SemDoc should have comment or be unexported, e o staticcheck, mesmo com todas as verificações ligadas, não disse nada sobre ela. Uma verificação automática no CI é mais confiável que um item de checklist, porque roda em todo PR e não depende da atenção de quem revisa.

Comentários

Mais em Golang

O pacote context: propagação de cancelamento e prazos
O pacote context: propagação de cancelamento e prazos

O pacote context em Go: Background e TODO, WithCancel, WithTimeout e…

Leitura e escrita de arquivos com os pacotes os e bufio
Leitura e escrita de arquivos com os pacotes os e bufio

Arquivos em Go: ReadFile e WriteFile, Open, Create e OpenFile, bufio.Scanner e…

Arrays e Slices: a espinha dorsal das coleções em Go
Arrays e Slices: a espinha dorsal das coleções em Go

Arrays e slices em Go: tamanho no tipo, len e cap, fatiamento que compartilha…