M01·04Boas Práticas Idiomáticas

CAPÍTULO 04

Boas Práticas Idiomáticas

Error handling, defer, organização de código e padrões comuns que fazem a diferença entre código iniciante e código de produção.

Por Thiago Souza10 min de leituraAtualizado em 2026-05

Error Handling: erros são valores, não exceções

Em Go, erros são apenas valores que você retorna e checa. Sem try/catch.

go
import (
    "errors"
    "fmt"
    "os"
)
 
func lerArquivo(caminho string) ([]byte, error) {
    dados, err := os.ReadFile(caminho)
    if err != nil {
        // Envolva o erro com contexto para facilitar debug
        return nil, fmt.Errorf("falha ao ler %s: %w", caminho, err)
    }
    return dados, nil
}
 
func main() {
    dados, err := lerArquivo("config.json")
    if err != nil {
        // Verificar tipo de erro
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("Arquivo não encontrado, criando padrão...")
            return
        }
        fmt.Println("Erro inesperado:", err)
        return
    }
    fmt.Println("Conteúdo:", string(dados))
}

Erros customizados

go
// Definindo um tipo de erro com contexto rico
type ErroValidacao struct {
    Campo   string
    Motivo  string
}
 
// Para satisfazer a interface "error", precisa do método Error()
func (e *ErroValidacao) Error() string {
    return fmt.Sprintf("validação falhou no campo %s: %s", e.Campo, e.Motivo)
}
 
func validarEmail(email string) error {
    if email == "" {
        return &ErroValidacao{Campo: "email", Motivo: "vazio"}
    }
    return nil
}

Padrão idiomático: if err != nil

Sim, você vai escrever isso toneladas de vezes. Não é feio, é explícito:

go
// O famoso "happy path" colado à esquerda
func process(dado string) error {
    parsed, err := parse(dado)
    if err != nil {
        return fmt.Errorf("parse: %w", err)
    }
 
    validated, err := validar(parsed)
    if err != nil {
        return fmt.Errorf("validar: %w", err)
    }
 
    if err := salvar(validated); err != nil {
        return fmt.Errorf("salvar: %w", err)
    }
 
    return nil
}

Defer: garantindo limpeza

defer adia a execução de uma função até o final da função atual. Perfeito para fechar recursos:

go
func copiarArquivo(origem, destino string) error {
    src, err := os.Open(origem)
    if err != nil {
        return err
    }
    defer src.Close() // será chamado quando a função retornar
 
    dst, err := os.Create(destino)
    if err != nil {
        return err
    }
    defer dst.Close() // mesma coisa
 
    _, err = io.Copy(dst, src)
    return err
}
Você nunca esquece de fechar arquivos, conexões de banco, mutexes. O defer roda mesmo se a função entrar em pânico (panic).

Organização de código

Estrutura de pastas típica de um projeto backend

meu-projeto/
├── go.mod              # define o módulo e dependências
├── go.sum              # hashes de verificação
├── cmd/
│   └── api/
│       └── main.go     # ponto de entrada
├── internal/           # código privado do projeto
│   ├── handler/        # camada HTTP
│   ├── service/        # regras de negócio
│   ├── repository/     # acesso a dados
│   └── model/          # structs de domínio
├── pkg/                # código reutilizável (público)
└── configs/
    └── config.yaml
A pasta internal/ é especial — só código DENTRO do seu projeto pode importar dela. O Go bloqueia importações externas. Use isso para esconder detalhes.

Convenções de nomes

O quêConvençãoExemplo
Pacoteminúsculo, curto, sem underscoreshttputil, jwt
Função/variável exportada (pública)PascalCase (começa maiúscula)BuscarUsuario
Função/variável interna (privada)camelCase (começa minúscula)validarEmail
Interfacenormalmente termina em -erReader, Writer, Notifier
ConstantesPascalCase ou SCREAMINGMaxConexoes, DEFAULT_PORT
Visibilidade em Go: é só pela primeira letra. Maiúscula = pública. Minúscula = privada (do pacote). Não tem public/private. Genial.

Padrões comuns idiomáticos

Construtor: função New...

Go não tem construtores, mas a convenção é uma função New<Tipo>:

go
type Server struct {
    port int
    db   *DB
}
 
func NewServer(port int, db *DB) *Server {
    return &Server{
        port: port,
        db:   db,
    }
}
 
// Uso
srv := NewServer(8080, myDB)

Receiver pequeno e consistente

Use a primeira letra do tipo como nome do receiver — sempre o mesmo dentro de um arquivo:

go
// ✅ Bom
func (u *User) Salvar() error { ... }
func (u *User) Validar() error { ... }
 
// ❌ Ruim — inconsistente
func (user *User) Salvar() error { ... }
func (this *User) Validar() error { ... }

Aceite interfaces, retorne structs

go
// ✅ Aceita qualquer coisa que satisfaça io.Reader
func process(r io.Reader) (*Result, error)
 
// ❌ Restringe demais — só aceita esse tipo específico
func process(r *bytes.Buffer) (*Result, error)