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

Injeção de dependência sem frameworks
Injeção de dependência sem frameworks

Injeção de dependência em Go sem framework: construtores que validam o que é…

GORM: ORM para Go, migrações e relacionamentos
GORM: ORM para Go, migrações e relacionamentos

GORM na prática: modelos, relacionamentos, scopes, transações com FOR UPDATE…

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…