Receivers por valor vs por ponteiro
1. Introdução aos Receivers em Go
Em Go, um receiver é um parâmetro especial que conecta uma função a um tipo, transformando-a em um método. A sintaxe básica coloca o receiver entre a palavra-chave func e o nome do método:
type Pessoa struct {
Nome string
Idade int
}
// Método com receiver por valor
func (p Pessoa) Saudacao() string {
return "Olá, eu sou " + p.Nome
}
A diferença fundamental entre funções e métodos é que métodos têm acesso ao estado do receiver. Enquanto uma função comum opera apenas com seus parâmetros explícitos, um método pode ler e modificar os campos do tipo ao qual está associado.
Os receivers são essenciais para implementar interfaces em Go. Uma interface é satisfeita implicitamente quando um tipo implementa todos os métodos declarados nela, e o tipo do receiver (valor ou ponteiro) determina se um valor ou ponteiro do tipo pode ser usado como a interface.
2. Receiver por Valor
Quando um método é declarado com receiver por valor, uma cópia da struct é passada para o método. Qualquer modificação feita dentro do método afeta apenas a cópia local, não a variável original.
type Contador struct {
Valor int
}
func (c Contador) Incrementar() {
c.Valor++ // Modifica apenas a cópia
}
func (c Contador) ObterValor() int {
return c.Valor // Leitura segura
}
func main() {
c := Contador{Valor: 10}
c.Incrementar()
fmt.Println(c.ObterValor()) // Ainda imprime 10
}
O comportamento de cópia torna receivers por valor ideais para métodos que apenas leem os campos da struct. Eles oferecem previsibilidade: quem chama o método sabe que a struct original não será alterada.
3. Receiver por Ponteiro
Com receiver por ponteiro, o método recebe o endereço da struct original. Isso permite modificar diretamente os campos da instância que chamou o método.
type Contador struct {
Valor int
}
func (c *Contador) Incrementar() {
c.Valor++ // Modifica o original
}
func (c *Contador) ObterValor() int {
return c.Valor
}
func main() {
c := Contador{Valor: 10}
c.Incrementar()
fmt.Println(c.ObterValor()) // Imprime 11
}
Note que Go permite chamar métodos com receiver ponteiro mesmo em variáveis do tipo valor (como c.Incrementar() sem usar &c). O compilador faz a conversão automaticamente.
Métodos setter são o exemplo clássico de uso de receiver por ponteiro:
type Pessoa struct {
nome string
}
func (p *Pessoa) SetNome(novoNome string) {
p.nome = novoNome
}
4. Quando Usar Receiver por Valor
Structs pequenas e imutáveis: tipos como time.Time usam receiver por valor porque são pequenos (apenas alguns campos) e imutáveis por design.
type Ponto struct {
X, Y float64
}
func (p Ponto) DistanciaOrigem() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
Métodos que não alteram estado: se o método é puramente de leitura, receiver por valor comunica essa intenção claramente.
Segurança e previsibilidade: quando você não quer que o método tenha efeitos colaterais na struct original, receiver por valor é a escolha segura.
Tipos básicos: para tipos como int, string ou bool, receivers por valor são a norma.
5. Quando Usar Receiver por Ponteiro
Necessidade de modificar o receiver: qualquer método que precise alterar campos da struct deve usar receiver por ponteiro.
func (buf *Buffer) Write(p []byte) (n int, err error) {
buf.data = append(buf.data, p...)
return len(p), nil
}
Structs grandes: copiar uma struct com muitos campos (ex: struct com 100KB) a cada chamada de método é ineficiente. Use ponteiro para evitar cópias desnecessárias.
type GrandeStruct struct {
Dados [10000]byte
}
func (g *GrandeStruct) Processar() {
// Evita copiar 10KB a cada chamada
}
Consistência com interfaces: se um método da interface exige receiver ponteiro, todos os métodos do tipo devem ser consistentes.
6. Implicações na Implementação de Interfaces
A escolha entre receiver por valor e por ponteiro afeta quais tipos satisfazem uma interface.
type Stringer interface {
String() string
}
type MeuTipo struct {
Valor string
}
// Receiver por valor
func (m MeuTipo) String() string {
return m.Valor
}
Neste caso, tanto MeuTipo quanto *MeuTipo satisfazem Stringer. Mas se o método usar receiver por ponteiro:
func (m *MeuTipo) String() string {
return m.Valor
}
Apenas *MeuTipo satisfaz Stringer. Um valor MeuTipo{} não pode ser usado onde Stringer é esperado.
Regra prática: se você tem um método com receiver por ponteiro, apenas ponteiros para o tipo implementam a interface. Isso é crucial ao trabalhar com interfaces como io.Writer:
type Writer interface {
Write(p []byte) (n int, err error)
}
type MeuWriter struct {
buffer []byte
}
func (w *MeuWriter) Write(p []byte) (n int, err error) {
w.buffer = append(w.buffer, p...)
return len(p), nil
}
Apenas *MeuWriter implementa io.Writer.
7. Boas Práticas e Armadilhas Comuns
Consistência: todos os métodos de um tipo devem usar o mesmo tipo de receiver. Misturar valor e ponteiro causa confusão e pode quebrar a implementação de interfaces.
// RUIM
type Exemplo struct {}
func (e Exemplo) MetodoA() {} // valor
func (e *Exemplo) MetodoB() {} // ponteiro
// BOM
type Exemplo struct {}
func (e *Exemplo) MetodoA() {}
func (e *Exemplo) MetodoB() {}
Nil receivers: métodos com receiver ponteiro podem ser chamados em valores nil. Isso pode ser útil, mas requer verificação explícita:
type Lista struct {
Prox *Lista
Valor int
}
func (l *Lista) Tamanho() int {
if l == nil {
return 0
}
return 1 + l.Prox.Tamanho()
}
Slices e maps: receivers por valor ainda copiam o header da slice (ponteiro, tamanho, capacidade), mas o array subjacente é compartilhado. Modificações nos elementos são visíveis fora do método:
func (s []int) AdicionarItem() {
s[0] = 99 // Modifica o array original
}
8. Resumo e Decisão Rápida
Use esta checklist para decidir:
| Situação | Receiver |
|---|---|
| Método modifica o receiver | Ponteiro |
| Struct grande (> 64 bytes) | Ponteiro |
| Struct contém mutex ou similar | Ponteiro |
| Método apenas lê (getter) | Valor |
| Struct pequena e imutável | Valor |
| Tipo básico (int, string) | Valor |
| Consistência com outros métodos | Mesmo tipo |
Casos especiais: tipos como int, string, bool e float sempre usam receiver por valor. A biblioteca padrão usa receiver por valor para time.Time e receiver por ponteiro para bytes.Buffer.
A regra de ouro do Go: se você não tem certeza, use receiver por ponteiro. É mais seguro para performance e mutabilidade, e você sempre pode mudar para valor depois se necessário.
Referências
- Effective Go: Methods — Documentação oficial explicando métodos, receivers e boas práticas
- Go by Example: Methods — Exemplos práticos de métodos com receivers por valor e ponteiro
- Dave Cheney: Pointers vs Values — Artigo técnico detalhado sobre quando usar ponteiros vs valores em Go
- The Go Blog: Methods, Interfaces and Embedding — Post oficial sobre métodos, interfaces e embedding em Go
- Go Specification: Method declarations — Especificação oficial da linguagem sobre declaração de métodos
- Stack Overflow: Value receiver vs pointer receiver — Discussão comunitária com exemplos e explicações adicionais