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