O que é Encapsulation em Go?

Encapsulation (encapsulamento) em Go é o mecanismo de controlar a visibilidade e o acesso a tipos, campos, funções e métodos. Diferente de linguagens como Java ou C# que usam keywords como public, private e protected, Go usa uma regra simples e elegante: a capitalização da primeira letra determina se um identificador é exported (público) ou unexported (privado).

Se o nome começa com letra maiúscula, é exported — visível fora do package. Se começa com letra minúscula, é unexported — visível apenas dentro do pacote onde foi definido. Essa regra se aplica a tudo: tipos, funções, métodos, campos de structs, constantes e variáveis.

package usuario

// Usuario é exported — visível fora do pacote
type Usuario struct {
    Nome  string // exported
    Email string // exported
    senha string // unexported — só acessível dentro do pacote 'usuario'
    idade int    // unexported
}

// NovoUsuario é exported — construtor público
func NovoUsuario(nome, email, senha string, idade int) *Usuario {
    return &Usuario{
        Nome:  nome,
        Email: email,
        senha: hashSenha(senha), // encapsulado
        idade: idade,
    }
}

// hashSenha é unexported — detalhe de implementação
func hashSenha(s string) string {
    // implementação interna
    return "hashed:" + s
}

Exported vs unexported na prática

Regra de capitalização

A regra é absoluta e aplicada pelo compilador — não há exceções:

package geometria

// Exported — acessível de qualquer pacote
type Circulo struct {
    Raio float64 // exported
}

func (c Circulo) Area() float64 {           // exported
    return math.Pi * c.Raio * c.Raio
}

func NovoCirculo(raio float64) Circulo {    // exported
    return Circulo{Raio: validarRaio(raio)}
}

// Unexported — só dentro do pacote 'geometria'
type pontoInterno struct { // unexported
    x, y float64          // unexported
}

func validarRaio(r float64) float64 {      // unexported
    if r < 0 {
        return 0
    }
    return r
}
package main

import "geometria"

func main() {
    c := geometria.NovoCirculo(5)   // OK
    fmt.Println(c.Raio)             // OK — campo exported
    fmt.Println(c.Area())           // OK — método exported

    // p := geometria.pontoInterno{} // ERRO: tipo unexported
    // geometria.validarRaio(5)       // ERRO: função unexported
}

Encapsulamento em campos de struct

Uma prática comum é exportar o tipo mas manter campos sensíveis unexported:

package banco

type Conta struct {
    Titular    string  // exported — informação pública
    Numero     string  // exported
    saldo      float64 // unexported — protegido
    transacoes []Transacao // unexported
}

type Transacao struct {
    Tipo  string
    Valor float64
    Data  time.Time
}

func NovaConta(titular, numero string, saldoInicial float64) *Conta {
    return &Conta{
        Titular: titular,
        Numero:  numero,
        saldo:   saldoInicial,
    }
}

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

func (c *Conta) Depositar(valor float64) error {
    if valor <= 0 {
        return fmt.Errorf("valor deve ser positivo: %.2f", valor)
    }
    c.saldo += valor
    c.transacoes = append(c.transacoes, Transacao{
        Tipo:  "depósito",
        Valor: valor,
        Data:  time.Now(),
    })
    return nil
}

func (c *Conta) Sacar(valor float64) error {
    if valor <= 0 {
        return fmt.Errorf("valor deve ser positivo")
    }
    if valor > c.saldo {
        return fmt.Errorf("saldo insuficiente: R$ %.2f", c.saldo)
    }
    c.saldo -= valor
    c.transacoes = append(c.transacoes, Transacao{
        Tipo:  "saque",
        Valor: valor,
        Data:  time.Now(),
    })
    return nil
}

O saldo só pode ser modificado através dos métodos Depositar e Sacar, que aplicam validações. Esse é o encapsulamento em ação.

Package-level encapsulation

Em Go, a unidade de encapsulamento é o package, não a struct ou o arquivo. Todos os arquivos dentro do mesmo pacote podem acessar identificadores unexported:

// arquivo: usuario/modelo.go
package usuario

type Usuario struct {
    Nome  string
    email string // unexported
}

// arquivo: usuario/validacao.go
package usuario

func validarEmail(u *Usuario) bool {
    return strings.Contains(u.email, "@") // OK — mesmo pacote
}

// arquivo: usuario/servico.go
package usuario

func (u *Usuario) AtualizarEmail(novo string) error {
    temp := &Usuario{email: novo}
    if !validarEmail(temp) { // OK — mesmo pacote
        return fmt.Errorf("email inválido: %s", novo)
    }
    u.email = novo
    return nil
}

Isso significa que organizar código em pacotes é uma decisão de design fundamental em Go. Cada pacote define uma fronteira de visibilidade.

Internal packages

A partir de Go 1.4, o diretório internal fornece encapsulamento adicional. Pacotes dentro de internal só podem ser importados por código no diretório pai:

projeto/
├── cmd/
│   └── app/
│       └── main.go          // pode importar internal/
├── internal/
│   ├── database/
│   │   └── db.go            // só acessível dentro de projeto/
│   └── auth/
│       └── auth.go          // só acessível dentro de projeto/
├── pkg/
│   └── api/
│       └── handler.go       // NÃO pode importar internal/
└── go.mod
// cmd/app/main.go
package main

import "projeto/internal/database" // OK — está dentro de projeto/

// Qualquer pacote externo:
// import "projeto/internal/database" // ERRO — violação de internal

Esse mecanismo é ideal para esconder detalhes de implementação que nenhum consumidor do module deveria usar. Muitos projetos Go de grande porte usam internal/ extensivamente.

Constructor functions (New*)

Como Go não tem construtores de classe, a convenção é criar funções construtoras que começam com New:

package config

type Config struct {
    host       string
    porta      int
    timeout    time.Duration
    maxRetries int
    logger     *log.Logger
}

// Construtor simples
func New(host string, porta int) *Config {
    return &Config{
        host:       host,
        porta:      porta,
        timeout:    30 * time.Second,
        maxRetries: 3,
        logger:     log.Default(),
    }
}

// Construtor com functional options (padrão avançado)
type Option func(*Config)

func WithTimeout(d time.Duration) Option {
    return func(c *Config) {
        c.timeout = d
    }
}

func WithMaxRetries(n int) Option {
    return func(c *Config) {
        c.maxRetries = n
    }
}

func WithLogger(l *log.Logger) Option {
    return func(c *Config) {
        c.logger = l
    }
}

func NewWithOptions(host string, porta int, opts ...Option) *Config {
    c := New(host, porta)
    for _, opt := range opts {
        opt(c)
    }
    return c
}

Uso:

cfg := config.NewWithOptions("localhost", 8080,
    config.WithTimeout(10*time.Second),
    config.WithMaxRetries(5),
)

O padrão functional options é amplamente utilizado na comunidade Go e em bibliotecas populares. Para mais sobre organização de código, veja o tutorial de clean architecture com Go.

Getters e setters em Go

Em Go, a convenção para getters é não usar o prefixo “Get”. O nome do getter é simplesmente o nome do campo com letra maiúscula. Setters usam o prefixo “Set”:

type Pessoa struct {
    nome  string
    idade int
}

// Getter — sem prefixo "Get"
func (p *Pessoa) Nome() string {
    return p.nome
}

func (p *Pessoa) Idade() int {
    return p.idade
}

// Setter — com prefixo "Set"
func (p *Pessoa) SetNome(nome string) {
    p.nome = strings.TrimSpace(nome)
}

func (p *Pessoa) SetIdade(idade int) error {
    if idade < 0 || idade > 150 {
        return fmt.Errorf("idade inválida: %d", idade)
    }
    p.idade = idade
    return nil
}

Importante: em Go idiomático, não crie getters e setters para todos os campos. Use-os apenas quando há lógica de validação, transformação ou quando precisa manter a flexibilidade de mudar a implementação interna. Exportar campos diretamente é perfeitamente aceitável quando não há invariantes a proteger.

// OK — campos simples exportados diretamente
type Ponto struct {
    X float64
    Y float64
}

// OK — getter com lógica (campo calculado)
type Retangulo struct {
    Largura float64
    Altura  float64
}

func (r Retangulo) Area() float64 {
    return r.Largura * r.Altura
}

Design de API com encapsulamento

Expondo interfaces, escondendo implementações

Um padrão poderoso em Go é exportar uma interface mas manter a implementação unexported:

package storage

// Storage é exported — a API pública
type Storage interface {
    Get(key string) ([]byte, error)
    Set(key string, value []byte) error
    Delete(key string) error
}

// memStorage é unexported — detalhe de implementação
type memStorage struct {
    dados map[string][]byte
    mu    sync.RWMutex
}

func (m *memStorage) Get(key string) ([]byte, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    val, ok := m.dados[key]
    if !ok {
        return nil, fmt.Errorf("chave não encontrada: %s", key)
    }
    return val, nil
}

func (m *memStorage) Set(key string, value []byte) error {
    m.mu.Lock()
    defer m.mu.Unlock()
    m.dados[key] = value
    return nil
}

func (m *memStorage) Delete(key string) error {
    m.mu.Lock()
    defer m.mu.Unlock()
    delete(m.dados, key)
    return nil
}

// NewMemStorage retorna a interface, não o tipo concreto
func NewMemStorage() Storage {
    return &memStorage{
        dados: make(map[string][]byte),
    }
}

Esse padrão permite trocar a implementação (memória, Redis, PostgreSQL) sem afetar o código consumidor. É fundamental em testes e em microserviços.

Error types encapsulados

Encapsulamento também se aplica a tipos de error:

package auth

// ErrNaoAutorizado é exported — consumidores podem verificar
var ErrNaoAutorizado = errors.New("não autorizado")

// detalhesErro é unexported — detalhes internos protegidos
type detalhesErro struct {
    codigo  int
    mensagem string
    interno  error
}

func (e *detalhesErro) Error() string {
    return e.mensagem
}

func (e *detalhesErro) Unwrap() error {
    return e.interno
}

Boas práticas de encapsulamento

  1. Exporte o mínimo necessário — comece com tudo unexported e exporte conforme a necessidade
  2. Use construtores — funções New* permitem validação e inicialização segura
  3. Organize pacotes por domínio — não por tipo técnico (evite pacotes genéricos como utils)
  4. Use internal/ — para código que não deve ser usado por consumidores do módulo
  5. Interfaces na fronteira — exporte interfaces, esconda implementações
  6. Evite getters/setters desnecessários — exporte campos quando não há invariantes
  7. Documente a API pública — todo identificador exported deve ter comentário

Para mais sobre organização de pacotes, veja o tutorial de Go modules na prática e o guia sobre boas práticas de erros.

Perguntas frequentes (FAQ)

Como funciona a visibilidade em Go sem public/private?

Go usa a capitalização da primeira letra. Nomes que começam com letra maiúscula são exported (visíveis fora do package), e nomes com letra minúscula são unexported (visíveis apenas dentro do pacote). Essa regra é aplicada pelo compilador e vale para tipos, funções, métodos, campos e constantes. É uma das convenções mais simples e consistentes da linguagem.

Qual a diferença entre unexported e internal packages?

Identificadores unexported (letra minúscula) são invisíveis fora do pacote onde foram definidos. Internal packages vão além: pacotes dentro de um diretório internal/ só podem ser importados por código no diretório pai. Unexported protege detalhes dentro de um pacote; internal/ protege pacotes inteiros dentro de um module.

Devo criar getters e setters para todos os campos?

Não. Em Go idiomático, só crie getters e setters quando há lógica de validação, transformação ou quando precisa manter flexibilidade para mudar a implementação. Exportar campos diretamente é perfeitamente aceitável quando não há invariantes a proteger. A convenção é usar Nome() (sem “Get”) para getters e SetNome() para setters.

Como o encapsulamento funciona com testes?

Testes no mesmo pacote (arquivo _test.go com package x) acessam identificadores unexported normalmente. Testes em pacote separado (package x_test) só acessam a API pública — isso é chamado de “black-box testing”. Ambas as abordagens são válidas e servem propósitos diferentes: testes internos verificam implementação, testes externos verificam a API.