Pacotes: visibilidade, nomenclatura e organização de projeto

Pacotes: visibilidade, nomenclatura e organização de projeto

Pacotes em Go: exportado e não exportado pela primeira letra, nomes curtos e sem repetição, alias, import em branco para drivers e import com ponto, internal/, estruturas com cmd/, a ordem real dos init, testes internos e externos com export_test.go e como desfazer ciclos e pacotes util.
Golang

• • 15 min de leitura

Em Go, todo arquivo fonte pertence a um pacote. Não existe código solto fora de um pacote — a declaração package nome na primeira linha de cada arquivo é obrigatória. Pacotes são simultaneamente a unidade de compilação, a unidade de encapsulamento e a unidade de reutilização da linguagem.

Entender como pacotes funcionam é compreender como Go organiza, expõe e protege código. Um projeto bem estruturado em pacotes é mais fácil de testar, mais fácil de manter e mais fácil de evoluir do que um projeto onde tudo está acumulado em um único arquivo ou pacote.

Visibilidade: exportado vs não exportado

Go usa uma regra simples e elegante para controlar visibilidade: a primeira letra do identificador determina se ele é público ou privado.

Identificadores que começam com letra maiúscula são exportados — acessíveis por qualquer pacote que importe o pacote onde foram definidos. Identificadores que começam com letra minúscula são não exportados — acessíveis apenas dentro do próprio pacote.

Essa regra se aplica a tudo: funções, tipos, variáveis, constantes, campos de struct e métodos.

package financeiro

import "fmt"

// Exportados — visíveis fora do pacote
type Conta struct {
    Titular string   // exportado
    saldo   float64  // não exportado — privado ao pacote
}

const TaxaJuros = 0.015  // exportado

// Exportado
func NovaConta(titular string, deposito float64) *Conta {
    return &Conta{
        Titular: titular,
        saldo:   deposito,
    }
}

// Exportado
func (c *Conta) Depositar(valor float64) error {
    if valor <= 0 {
        return fmt.Errorf("valor inválido: %.2f", valor)
    }
    c.saldo += valor
    return nil
}

// Exportado
func (c *Conta) Saldo() float64 {
    return c.saldo
}

// Não exportado — uso interno do pacote
func (c *Conta) aplicarTaxa() {
    c.saldo -= c.saldo * TaxaJuros
}

Código em outro pacote que importar financeiro poderá usar NovaConta, Depositar, Saldo e TaxaJuros, mas não poderá acessar saldo diretamente nem chamar aplicarTaxa. Esse encapsulamento protege invariantes internas e permite refatorar a implementação sem quebrar o código que usa o pacote.

Nomenclatura de pacotes

A comunidade Go consolidou convenções fortes para nomear pacotes. Segui-las torna o código instantaneamente reconhecível como Go idiomático.

Nomes curtos, minúsculos e sem underscores ou camelCase:

// Correto
package http
package json
package usuario
package pedido

// Evite
package HttpClient
package user_service
package gestãoDePedidos

O nome do pacote deve descrever o que ele fornece, não o que ele faz:

// Bom — descreve o que é
package crypto
package parser
package cache

// Evite — genérico demais
package util
package helpers
package common
package misc

Nomes como util e helpers são sintomas de um pacote que cresceu sem propósito claro. Se um pacote se chama util, é provável que ele deveria ser dividido em pacotes menores com nomes mais específicos.

O nome do pacote é parte da API. Quando um usuário chama json.Marshal, o nome json faz parte da expressão. Por isso, evite redundância entre o nome do pacote e os identificadores que ele exporta:

// Ruim — redundante: usuario.UsuarioService, usuario.UsuarioID
package usuario
type UsuarioService struct{}
type UsuarioID int

// Bom — natural: usuario.Service, usuario.ID
package usuario
type Service struct{}
type ID int

Importando pacotes

A declaração import traz pacotes para o escopo do arquivo. A forma padrão usa o caminho do módulo:

import (
    "fmt"
    "net/http"
    "encoding/json"

    "github.com/gin-gonic/gin"
    "meu-projeto/internal/usuario"
)

Alias de importação. Quando dois pacotes têm o mesmo nome, ou quando o nome padrão é inconveniente, um alias resolve a ambiguidade:

import (
    "crypto/rand"
    mrand "math/rand"  // alias para distinguir dos dois "rand"
)

func main() {
    // rand do crypto
    b := make([]byte, 16)
    rand.Read(b)

    // rand do math
    n := mrand.Intn(100)
    fmt.Println(n)
}

Importação em branco. Às vezes um pacote precisa ser importado apenas pelos efeitos colaterais de seu init — por exemplo, registrar um driver de banco de dados — sem usar nenhum identificador exportado. O _ evita o erro de importação não usada:

import (
    "database/sql"
    _ "github.com/lib/pq"  // registra o driver PostgreSQL via init()
)

Importação com ponto. Importa todos os identificadores exportados diretamente no escopo atual, sem prefixo. Raramente usada em código de produção, mais comum em testes:

import . "fmt"

func main() {
    Println("sem prefixo fmt")  // em vez de fmt.Println
}

O diretório internal

Go possui um mecanismo de visibilidade a nível de diretório chamado internal. Pacotes dentro de um diretório internal só podem ser importados por código dentro da árvore de diretórios pai do internal:

meu-projeto/
├── go.mod
├── main.go
├── internal/
│   ├── banco/
│   │   └── banco.go      ← só importável dentro de meu-projeto/
│   └── config/
│       └── config.go     ← só importável dentro de meu-projeto/
└── pkg/
    └── cliente/
        └── cliente.go    ← importável por qualquer projeto externo

Se outro projeto tentar importar meu-projeto/internal/banco, o compilador recusará com erro. Esse mecanismo é poderoso para criar APIs internas sem expô-las publicamente — a alternativa ao internal seria deixar o código acessível e confiar apenas em documentação para indicar que não deve ser usado externamente.

Estruturas de projeto recomendadas

Go não impõe uma estrutura de diretórios obrigatória além da localização do go.mod. A comunidade converge em torno de algumas convenções práticas.

Projeto simples — utilitário ou microsserviço:

meu-servico/
├── go.mod
├── go.sum
├── main.go
├── handler/
│   └── handler.go
├── service/
│   └── service.go
└── repository/
    └── repository.go

Projeto com múltiplos binários:

meu-projeto/
├── go.mod
├── go.sum
├── cmd/
│   ├── api/
│   │   └── main.go       ← binário da API
│   └── worker/
│       └── main.go       ← binário do worker
├── internal/
│   ├── config/
│   ├── database/
│   └── domain/
└── pkg/
    └── middleware/

O diretório cmd/ contém os pontos de entrada (main.go) de cada binário. Toda a lógica real fica em internal/ ou pkg/, mantendo os main.go enxutos — idealmente com apenas a montagem das dependências e a chamada de inicialização.

Projeto com domínios complexos — inspirado em Clean Architecture:

meu-projeto/
├── go.mod
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── domain/
│   │   ├── usuario.go
│   │   └── pedido.go
│   ├── usecase/
│   │   ├── criar_usuario.go
│   │   └── processar_pedido.go
│   ├── repository/
│   │   ├── usuario_repo.go
│   │   └── pedido_repo.go
│   └── handler/
│       ├── usuario_handler.go
│       └── pedido_handler.go
└── pkg/
    ├── validator/
    └── logger/

Inicialização de pacotes com init

Como visto no artigo sobre funções, cada pacote pode ter funções init que executam antes de main. A ordem de execução segue as dependências, e não a posição das linhas de import: todo pacote é inicializado antes de quem o importa. Se o main importa b antes de a, mas b também importa a, o init de a roda primeiro, e cada pacote é inicializado uma vez só, por mais que seja importado.

// pacote config
package config

import "fmt"

var Ambiente string

func init() {
    Ambiente = "desenvolvimento"
    fmt.Println("config: inicializado")
}
// pacote main
package main

import (
    "fmt"
    "meu-projeto/internal/config"
)

func init() {
    fmt.Println("main: init executado, ambiente:", config.Ambiente)
}

func main() {
    fmt.Println("main: executando")
}

// Saída:
// config: inicializado
// main: init executado, ambiente: desenvolvimento
// main: executando

Pacotes e testabilidade

Uma consequência direta de uma boa estrutura de pacotes é a facilidade de testar. Pacotes com responsabilidades claras e bem delimitadas têm interfaces menores, dependências explícitas e são mais fáceis de isolar em testes.

Os arquivos de teste terminam em _test.go e ficam na mesma pasta do código testado. Dentro deles há duas opções de pacote: o mesmo nome do código, package financeiro, que enxerga tudo, inclusive o que não é exportado; ou o nome com o sufixo _test, que roda como um pacote externo:

// arquivo: financeiro/conta_test.go
package financeiro_test  // teste externo — acessa apenas a API pública

import (
    "testing"
    "meu-projeto/internal/financeiro"
)

func TestDepositar(t *testing.T) {
    conta := financeiro.NovaConta("Ricardo", 100.0)

    if err := conta.Depositar(50.0); err != nil {
        t.Fatalf("erro inesperado: %v", err)
    }

    if conta.Saldo() != 150.0 {
        t.Errorf("esperado 150.0, obtido %.2f", conta.Saldo())
    }
}

Usar package financeiro_test em vez de package financeiro força o teste a interagir apenas com a API pública — o que é uma verificação valiosa de que a API pública é suficiente e bem desenhada.

Documentação de pacotes

Todo pacote exportado deve ter um comentário de documentação começando com Package nome:

// Package financeiro fornece tipos e operações para gestão de contas bancárias,
// incluindo depósitos, saques e aplicação de taxas de juros.
package financeiro

Funções e tipos exportados devem ter comentários que começam com o nome do identificador:

// NovaConta cria e retorna uma nova conta bancária para o titular informado,
// com o depósito inicial fornecido.
func NovaConta(titular string, deposito float64) *Conta {

Esses comentários são exibidos pelo go doc e pelo site pkg.go.dev automaticamente, formando a documentação oficial do pacote.

Pacotes são a fronteira que o Go usa para quase tudo: visibilidade decidida pela primeira letra, compilação em ordem de dependência, internal/ imposto pelo compilador, testes que escolhem ver o pacote por dentro ou por fora. Bons pacotes têm nomes curtos que dizem o que fornecem e dependências que apontam num sentido só; quando aparece um ciclo de importação ou um util que todos importam, é o desenho que está pedindo revisão, e não a sintaxe.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Um serviço importa o driver do PostgreSQL com _ "github.com/lib/pq". Num refactoring, o editor remove essa linha "porque o pacote não é usado". O código compila, mas o serviço falha ao subir. Qual é a mensagem, e por que o compilador não pôde avisar?

Ver resposta

✓ Resposta: A falha aparece no sql.Open: sql: unknown driver "postgres" (forgotten import?), e a própria mensagem sugere a causa. O database/sql não conhece nenhum banco: cada driver se registra, com sql.Register("postgres", ...), dentro da sua função init, e o init só roda se o pacote do driver for importado por alguém. A importação em branco existe justamente para isso: trazer o pacote sem usar nenhum identificador dele. O compilador não pode avisar porque, do ponto de vista dos tipos, o programa está completo: o nome do driver é uma string que só é consultada em tempo de execução. A defesa prática é deixar o import em branco no arquivo que monta a conexão, e não num arquivo qualquer, com um comentário dizendo por que ele está lá, além de um teste de integração que abra a conexão. Vale notar também que o lib/pq está em modo de manutenção, e o próprio projeto recomenda o pgx para código novo.

Exercício 2

Os pacotes usuario e pedido precisam um do outro: um usuário lista seus pedidos, e um pedido mostra o nome do usuário. Ao fazer cada um importar o outro, o build falha. Qual é o erro, e quais são as saídas de desenho?

Ver resposta

✓ Resposta: O Go proíbe ciclos de importação, e o build para com import cycle not allowed, mostrando a cadeia inteira. A proibição é o que permite inicializar os pacotes em ordem de dependência, como o artigo descreve, e compilá-los um de cada vez. As saídas são de desenho, não de sintaxe. A primeira é inverter a dependência com uma interface: pedido declara type BuscadorDeNome interface { Nome(id int) string } e recebe uma implementação, que vem de usuario, sem importá-lo. A segunda é extrair o que os dois compartilham para um terceiro pacote, geralmente de tipos de domínio, que ambos importam e que não importa nenhum deles. A terceira, quando os dois vivem sempre juntos, é admitir que são um pacote só. Em Go, o ciclo costuma ser sinal de que a divisão em pacotes seguiu a estrutura de pastas de outra linguagem, e não as dependências reais do código.

Exercício 3

Os testes de financeiro usam package financeiro_test, como o artigo recomenda, mas um deles precisa exercitar o método não exportado aplicarTaxa. O acesso direto não compila. Como testar sem exportar o método para todo mundo?

Ver resposta

✓ Resposta: O pacote _test é, para o compilador, um pacote externo como qualquer outro: acessar um campo ou método minúsculo dá, por exemplo, c.saldo undefined (cannot refer to unexported field saldo). A saída convencional é o arquivo export_test.go. Ele fica na mesma pasta, declara package financeiro, o pacote interno, e expõe só o necessário: func (c *Conta) AplicarTaxa() { c.aplicarTaxa() }. Como todo arquivo terminado em _test.go, ele só entra na compilação durante o go test. Num teste medido, o go test passou usando o método exportado, e um main que tentou chamá-lo não compilou, com has no field or method Taxa, but does have unexported method taxa. É a própria biblioteca padrão que usa esse truque. A alternativa é escrever esse teste específico num arquivo com package financeiro; os dois estilos convivem na mesma pasta.

Exercício 4

Um projeto tem um pacote util com 40 funções: formatação de CPF, cálculo de frete, parsing de datas, envio de e-mail e retentativas HTTP. Todo pacote do projeto importa util. Que problemas concretos isso causa, além do nome genérico que o artigo critica, e como começar a desmontá-lo?

Ver resposta

✓ Resposta: O primeiro problema é de dependência: como util envia e-mail e faz HTTP, ele importa bibliotecas de rede e de SMTP, e qualquer pacote que só queria formatar um CPF passa a depender delas também, o que pesa no build, nos testes e no binário. O segundo é de ciclo: basta uma função de util precisar de um tipo de domínio para surgir o import cycle not allowed, já que o domínio importa util. O terceiro é de leitura: util.Formatar(x) não diz nada sobre o que é formatado, enquanto cpf.Formatar(x) diz. Para desmontar, agrupe as funções pelo assunto e crie pacotes pequenos com nomes que descrevem o que fornecem, como cpf, frete, email e retry, movendo um grupo de cada vez, com os testes rodando a cada passo. Funções usadas por um pacote só voltam para dentro dele, sem exportar. O objetivo não é zerar o util num dia, e sim parar de alimentá-lo.

Exercício 5

O pacote b importa a, e o main importa b e depois a, nessa ordem no bloco de import. Os três têm init que imprimem uma linha. Em que ordem as mensagens aparecem, quantas vezes o init de a roda, e por que um sistema que depende dessa ordem para funcionar está frágil?

Ver resposta

✓ Resposta: A saída é init a, init b, init main e só então o main. A posição das linhas no bloco de import não conta: b depende de a, então a precisa estar pronto antes, e o main vem por último porque depende de ambos. O init de a roda uma vez só, embora a seja importado duas vezes, porque cada pacote é inicializado uma única vez por programa. A fragilidade está em que a ordem é consequência do grafo de dependências, e o grafo muda sem que ninguém perceba: um import acrescentado num terceiro pacote pode reordenar inicializações entre pacotes que não dependem um do outro. Se b precisa que a tenha configurado algo, a dependência deveria estar no código, com b chamando uma função de a, e não implícita num efeito colateral de init. É mais um argumento do artigo para usar init com moderação.

Comentários

Mais em Golang

Trabalhando com JSON: encode, decode e tags de struct
Trabalhando com JSON: encode, decode e tags de struct

JSON em Go: Marshal e Unmarshal, tags com omitempty, omitzero, string e…

gRPC e Protocol Buffers: comunicação eficiente entre serviços
gRPC e Protocol Buffers: comunicação eficiente entre serviços

gRPC em Go com Protocol Buffers: contrato e código gerado, interceptors com…

Structs: definição, inicialização e campos embutidos
Structs: definição, inicialização e campos embutidos

Structs em Go: literais com nome e posicionais, valor zero, ponteiros, structs…