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
-
Effective Go — Package names — Convenções oficiais de nomenclatura de pacotes. https://go.dev/doc/effective_go#package-names
-
Go Blog: Organizing a Go module — Guia oficial para estruturar projetos Go. https://go.dev/doc/modules/layout
-
Go Blog: Package names — Artigo detalhado sobre como nomear pacotes idiomaticamente. https://go.dev/blog/package-names
-
Standard Go Project Layout — Referência comunitária amplamente adotada para estrutura de projetos. https://github.com/golang-standards/project-layout
-
Documentação sobre internal packages — Regras oficiais do mecanismo internal. https://pkg.go.dev/cmd/go#hdr-Internal_Directories
-
go doc — Documentação de linha de comando — Como usar o comando go doc para explorar pacotes. https://pkg.go.dev/cmd/doc
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.