Golang

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

9 min de leitura

Pacotes: visibilidade, nomenclatura e organização de projeto
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,

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. Seguí-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 é determinada pela ordem de importação: os pacotes importados são inicializados antes do pacote que os importa.

// 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.

O padrão mais comum é colocar os testes no mesmo pacote com o sufixo _test:

// 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. Retorna erro se o depósito for negativo.
func NovaConta(titular string, deposito float64) (*Conta, error) {

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


Resumo do que foi coberto

Este artigo apresentou o sistema de pacotes do Go em profundidade: a regra de visibilidade por letra maiúscula, convenções de nomenclatura, formas de importação, o diretório internal para encapsulamento estrutural, estruturas de projeto recomendadas para diferentes escalas, a ordem de inicialização com init, a relação entre pacotes e testabilidade e a documentação de pacotes com comentários. O próximo artigo aprofunda o sistema de módulos — a camada que gerencia dependências entre pacotes.


Referências e leituras complementares

Comentários

Mais em Golang

Arrays e Slices: a espinha dorsal das coleções em Go
Arrays e Slices: a espinha dorsal das coleções em Go

Toda linguagem de programa&ccedil;&atilde;o precisa de uma forma de armazenar...

Artigo 28 — Criando um servidor HTTP com net/http
Artigo 28 — Criando um servidor HTTP com net/http

Artigo 28 — Criando um servidor HTTP com net/http O servidor HTTP que já vem...

Go com MySQL e MariaDB: conexão, queries e transações
Go com MySQL e MariaDB: conexão, queries e transações

MySQL é o banco de dados relacional mais usado no mundo, e MariaDB é seu fork...