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
- Exporte o mínimo necessário — comece com tudo unexported e exporte conforme a necessidade
- Use construtores — funções
New*permitem validação e inicialização segura - Organize pacotes por domínio — não por tipo técnico (evite pacotes genéricos como
utils) - Use
internal/— para código que não deve ser usado por consumidores do módulo - Interfaces na fronteira — exporte interfaces, esconda implementações
- Evite getters/setters desnecessários — exporte campos quando não há invariantes
- 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.