Escrever código Go idiomático é mais do que apenas seguir uma sintaxe; é adotar um conjunto de convenções e princípios que tornam o código claro, consistente e fácil de manter. Nesta aula, vamos explorar os fundamentos do Effective Go, as convenções de nomenclatura e formatação, a importância da simplicidade e como conduzir code reviews eficientes. Ao dominar esses aspectos, você será capaz de produzir código que não apenas funciona, mas que também é agradável de ler e colaborar.

Go foi projetado com a simplicidade em mente, e isso se reflete em suas convenções. A comunidade Go valoriza código legível, sem excessos, e que siga padrões bem estabelecidos. Vamos mergulhar em cada um desses tópicos para que você possa internalizar essas práticas.

Effective Go (princípios)

O documento Effective Go é a referência oficial da linguagem para escrever código idiomático. Ele traz diretrizes que vão desde formatação até design de APIs. O princípio central é que o código deve ser claro e expressivo, aproveitando os recursos da linguagem de forma que o leitor entenda rapidamente a intenção.

Um dos exemplos mais conhecidos do Effective Go é o tratamento de erros. Em Go, erros são valores e devem ser tratados explicitamente. Isso incentiva o programador a pensar sobre os caminhos de erro desde o início. Outro princípio é a composição: em vez de herança, Go usa interfaces e composição de structs para reutilizar código.

Vamos ver um exemplo de tratamento de erro idiomático:

package main

import (
    "fmt"
    "os"
)

func readFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("leitura de arquivo falhou: %w", err)
    }
    return data, nil
}

func main() {
    data, err := readFile("config.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "erro: %v\n", err)
        os.Exit(1)
    }
    fmt.Println(string(data))
}

Note como o erro é propagado com contexto adicional usando %w, permitindo que o chamador use errors.Is ou errors.As. Esse padrão é amplamente recomendado no Effective Go.

Convenções

As convenções em Go são rigorosas e amplamente adotadas. A mais importante é a formatação automática com gofmt. Todo código Go deve ser formatado com gofmt, pois isso elimina disputas sobre estilo e garante consistência em todos os projetos. Além disso, há convenções de nomenclatura: use CamelCase para nomes exportados (começando com maiúscula) e camelCase para não exportados. Nomes curtos e concisos são preferidos, especialmente para variáveis locais.

Outra convenção importante é a organização de imports: agrupe imports em blocos: primeiro os da biblioteca padrão, depois os de terceiros. O ferramenta goimports gerencia isso automaticamente. Nomes de funções devem ser verbos, e nomes de tipos devem ser substantivos. Por exemplo, func NewServer() ou type Server struct.

Exemplo de convenções de nomenclatura:

package main

// Exportado: começa com maiúscula
type Servidor struct {
    Porta int
}

// Não exportado: começa com minúscula
type config struct {
    timeout time.Duration
}

func (s *Servidor) Iniciar() error {
    // método exportado
    return nil
}

func (c *config) setDefault() {
    // método não exportado
}

Embora este código seja válido, a convenção em Go é usar nomes em inglês. Portanto, prefira Server e Start.

Simplicidade

Go promove a simplicidade: menos é mais. Isso significa evitar abstrações desnecessárias, preferir funções pequenas e claras, e usar as estruturas de dados da linguagem em vez de reinventar a roda. Por exemplo, em vez de criar hierarquias complexas de tipos, use interfaces pequenas e compostas.

Outra prática é preferir a concisão: use := para inferência de tipo, mas não abuse. Se a variável tem um tipo claro, a inferência é boa. Evite código rebuscado; o objetivo é que qualquer pessoa que leia entenda rapidamente.

Exemplo de simplicidade: em vez de um construtor com muitos parâmetros, use o padrão de opções funcionais, que é idiomático em Go.

type Config struct {
    Host string
    Port int
}

type Option func(*Config)

func WithHost(host string) Option {
    return func(c *Config) {
        c.Host = host
    }
}

func WithPort(port int) Option {
    return func(c *Config) {
        c.Port = port
    }
}

func NewServer(opts ...Option) *Server {
    cfg := &Config{Host: "localhost", Port: 8080}
    for _, opt := range opts {
        opt(cfg)
    }
    return &Server{cfg: cfg}
}

Esse padrão mantém a API limpa e flexível, sem sobrecarregar o construtor.

Code review

Code review é uma prática essencial para manter a qualidade do código. Em Go, isso envolve verificar se o código segue as convenções, se está claro e se não há erros comuns. Ao revisar, preste atenção a pontos como: o código é idiomático? Usa gofmt? Os erros são tratados adequadamente? Há testes suficientes?

Ferramentas como go vet e staticcheck ajudam a identificar problemas. Além disso, a revisão deve ser construtiva, sugerindo melhorias e explicando o porquê. Lembre-se de que o objetivo é o aprendizado e a melhoria contínua.

Um checklist básico para code review em Go:

  • O código está formatado com gofmt?
  • Os nomes são descritivos e seguem as convenções?
  • Os erros são tratados e propagados corretamente?
  • As funções são pequenas e fazem uma única coisa?
  • Há testes para os casos principais e de erro?
  • O código é simples e legível?

Exemplo de uma função que pode ser melhorada:

func process(data []byte) ([]byte, error) {
    if len(data) == 0 {
        return nil, errors.New("dados vazios")
    }
    // processa...
    return result, nil
}

Durante a revisão, você pode sugerir o uso de fmt.Errorf com contexto, ou a criação de um tipo de erro customizado.

Boas práticas e observações finais

Além dos tópicos principais, algumas práticas adicionais ajudam a escrever código Go idiomático: use goroutines com cautela, sempre evitando vazamentos; prefira canais para comunicação entre goroutines; e documente funções exportadas com comentários que comecem com o nome da função. A documentação é parte do idioma, e o go doc depende dela.

Lembre-se de que escrever código idiomático é uma jornada contínua. Leia código de projetos de sucesso, participe de comunidades e pratique. Com o tempo, essas convenções se tornarão naturais.

Referências

Exercícios

  1. Refatore o código abaixo para seguir as convenções de nomenclatura e formatação de Go. Explique as mudanças.
    package main
    
    import "fmt"
    
    type pessoa struct {
        Nome string
        idade int
    }
    
    func (p pessoa) getNome() string {
        return p.Nome
    }
    
    func (p *pessoa) setNome(n string) {
        p.Nome = n
    }
    
    func main() {
        p := pessoa{Nome: "João"}
        fmt.Println(p.getNome())
    }

    ✓ Resposta: O código deve usar nomes em inglês, métodos exportados começando com maiúscula e campos exportados também. Formate com gofmt. Exemplo:
    package main
    
    import "fmt"
    
    type Person struct {
        Name string
        age  int
    }
    
    func (p Person) GetName() string {
        return p.Name
    }
    
    func (p *Person) SetName(name string) {
        p.Name = name
    }
    
    func main() {
        p := Person{Name: "John"}
        fmt.Println(p.GetName())
    }
  2. Escreva uma função Divide que receba dois inteiros e retorne o quociente e um erro se o divisor for zero. Use o padrão idiomático de tratamento de erro.

✓ Resposta:
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("divisão por zero")
    }
    return a / b, nil
}
  • Dado o trecho de código abaixo, identifique três problemas de simplicidade e proponha melhorias.
    func Process(data []string) []string {
        result := make([]string, 0)
        for _, item := range data {
            if len(item) > 0 {
                trimmed := strings.TrimSpace(item)
                if trimmed != "" {
                    result = append(result, strings.ToUpper(trimmed))
                }
            }
        }
        return result
    }

    ✓ Resposta: Problemas: 1) A verificação len(item) > 0 é redundante, pois strings.TrimSpace já lida com isso; 2) A dupla verificação de vazio pode ser simplificada; 3) O código pode ser mais direto. Melhoria:
    func Process(data []string) []string {
        result := make([]string, 0)
        for _, item := range data {
            trimmed := strings.TrimSpace(item)
            if trimmed != "" {
                result = append(result, strings.ToUpper(trimmed))
            }
        }
        return result
    }
  • Durante um code review, você encontra o seguinte código. Liste três comentários construtivos que você faria.
    func GetUser(id int) (*User, error) {
        if id <= 0 {
            return nil, errors.New("id inválido")
        }
        // ... busca no banco
        return user, nil
    }

    ✓ Resposta: 1) Use fmt.Errorf para adicionar contexto ao erro; 2) Considere usar um tipo de erro customizado para erros de validação; 3) Garanta que a busca no banco trate erros de forma adequada, retornando um erro com contexto.
  • Implemente uma função Concat que concatena uma lista de strings com um separador, mas usando o padrão idiomático de variáveis e tratamento de erro. A função deve retornar um erro se a lista for vazia.
  • ✓ Resposta:
    func Concat(parts []string, sep string) (string, error) {
        if len(parts) == 0 {
            return "", errors.New("lista vazia")
        }
        return strings.Join(parts, sep), nil
    }