Métodos em Go são funções associadas a um tipo específico. Diferente de linguagens orientadas a objetos, Go não tem classes, mas permite definir métodos em qualquer tipo definido localmente (como structs, slices, maps, etc.). Um método recebe um argumento especial chamado receiver, que aparece entre a palavra-chave func e o nome do método.

O receiver pode ser um valor ou um ponteiro para o tipo. Essa escolha afeta como o método pode modificar o valor original e como ele é chamado. Nesta aula, exploraremos os dois tipos de receivers, quando usar cada um, e como criar métodos em tipos próprios.

Receivers (valor vs ponteiro)

Um receiver de valor recebe uma cópia do valor original. Qualquer modificação feita no valor dentro do método não afeta o valor original fora do método. Já um receiver de ponteiro recebe um ponteiro para o valor original, permitindo que o método modifique o valor original diretamente.

A sintaxe para definir um método com receiver de valor é: func (t Tipo) Metodo(). Para receiver de ponteiro: func (t *Tipo) Metodo(). O tipo do receiver pode ser qualquer tipo definido localmente, incluindo tipos primitivos, desde que seja definido com type.

package main

import "fmt"

type Pessoa struct {
    Nome string
    Idade int
}

// Receiver de valor
func (p Pessoa) Saudacao() string {
    return "Olá, meu nome é " + p.Nome
}

// Receiver de ponteiro
func (p *Pessoa) Aniversario() {
    p.Idade++
}

func main() {
    p := Pessoa{Nome: "João", Idade: 30}
    fmt.Println(p.Saudacao()) // Olá, meu nome é João
    p.Aniversario()
    fmt.Println(p.Idade) // 31
}

Quando usar cada um

Use receiver de valor quando o método não precisa modificar o valor original e o tipo é pequeno (por exemplo, tipos primitivos, structs pequenas). Também é adequado quando você quer garantir que o valor original não seja alterado.

Use receiver de ponteiro quando o método precisa modificar o valor original, ou quando o tipo é grande (para evitar cópias desnecessárias). Além disso, se um método precisa ser chamado em um valor que pode ser nulo (ponteiro), o receiver de ponteiro é necessário. Uma boa prática é ser consistente: se um tipo tiver algum método com receiver de ponteiro, todos os métodos desse tipo devem usar receiver de ponteiro.

package main

import "fmt"

type Conta struct {
    Saldo float64
}

// Receiver de valor - não modifica
func (c Conta) SaldoAtual() float64 {
    return c.Saldo
}

// Receiver de ponteiro - modifica
func (c *Conta) Depositar(valor float64) {
    c.Saldo += valor
}

func main() {
    conta := Conta{Saldo: 100.0}
    fmt.Println("Saldo:", conta.SaldoAtual()) // 100
    conta.Depositar(50.0)
    fmt.Println("Saldo após depósito:", conta.SaldoAtual()) // 150
}

Métodos em tipos próprios

Você pode definir métodos em qualquer tipo que você define localmente, não apenas structs. Por exemplo, você pode criar um tipo baseado em int ou string e adicionar métodos a ele. Isso permite estender tipos primitivos com comportamentos específicos.

package main

import "fmt"

type MeuInteiro int

func (m MeuInteiro) Dobro() MeuInteiro {
    return m * 2
}

func main() {
    var x MeuInteiro = 5
    fmt.Println(x.Dobro()) // 10
}

Lembre-se de que o tipo deve ser definido no mesmo pacote do método. Você não pode adicionar métodos a tipos de outros pacotes (como int diretamente).

Exemplos

Vamos ver um exemplo mais completo que demonstra o uso de receivers de valor e ponteiro em um tipo struct, incluindo métodos que retornam novos valores e métodos que modificam o original.

package main

import "fmt"

type Ponto struct {
    X, Y int
}

// Receiver de valor - retorna novo ponto deslocado
func (p Ponto) Deslocar(dx, dy int) Ponto {
    return Ponto{X: p.X + dx, Y: p.Y + dy}
}

// Receiver de ponteiro - modifica o ponto original
func (p *Ponto) Mover(dx, dy int) {
    p.X += dx
    p.Y += dy
}

func main() {
    p := Ponto{X: 1, Y: 2}
    p2 := p.Deslocar(3, 4)
    fmt.Printf("Original: (%d, %d)\n", p.X, p.Y)   // (1,2)
    fmt.Printf("Deslocado: (%d, %d)\n", p2.X, p2.Y) // (4,6)

    p.Mover(5, 6)
    fmt.Printf("Após mover: (%d, %d)\n", p.X, p.Y) // (6,8)
}

Boas Práticas

1. Se um método não modifica o receiver, use receiver de valor para tipos pequenos; para tipos grandes, use ponteiro para evitar cópias.
2. Se algum método do tipo precisar de receiver de ponteiro, todos os métodos desse tipo devem usar receiver de ponteiro para consistência.
3. Não use receiver de ponteiro para tipos que são referências (slices, maps, channels, funções) porque eles já são ponteiros internamente.
4. Evite misturar receivers de valor e ponteiro no mesmo tipo, a menos que tenha um motivo claro.

Referências

Exercícios

  1. Crie um tipo Circulo com campo Raio float64. Defina um método Area() float64 que retorna a área do círculo (π * raio²). Use receiver de valor.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "math"
    )
    
    type Circulo struct {
        Raio float64
    }
    
    func (c Circulo) Area() float64 {
        return math.Pi * c.Raio * c.Raio
    }
    
    func main() {
        c := Circulo{Raio: 5}
        fmt.Println(c.Area())
    }
  2. Para o tipo Circulo do exercício anterior, adicione um método Escalar(fator float64) que multiplica o raio pelo fator. Use receiver de ponteiro.

    ✓ Resposta:
    func (c *Circulo) Escalar(fator float64) {
        c.Raio *= fator
    }
    
    // Exemplo de uso:
    // c := Circulo{Raio: 5}
    // c.Escalar(2)
    // fmt.Println(c.Raio) // 10
  3. Crie um tipo Lista baseado em []int. Defina um método Soma() int que retorna a soma dos elementos. Use receiver de valor.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    type Lista []int
    
    func (l Lista) Soma() int {
        total := 0
        for _, v := range l {
            total += v
        }
        return total
    }
    
    func main() {
        l := Lista{1, 2, 3, 4}
        fmt.Println(l.Soma()) // 10
    }
  4. Para o tipo Lista acima, adicione um método Adicionar(valor int) que adiciona um elemento ao final da slice. Use receiver de ponteiro. (Dica: use append)

    ✓ Resposta:
    func (l *Lista) Adicionar(valor int) {
        *l = append(*l, valor)
    }
    
    // Exemplo:
    // l := Lista{1,2}
    // l.Adicionar(3)
    // fmt.Println(l) // [1 2 3]
  5. Explique por que o método Adicionar do exercício anterior precisa de um receiver de ponteiro, enquanto o método Soma pode usar receiver de valor.

    ✓ Resposta:O método Adicionar modifica a slice original (adiciona um elemento), o que requer uma alteração no cabeçalho da slice (ponteiro, comprimento e capacidade). Com receiver de valor, a função receberia uma cópia da slice header, e a modificação não seria refletida fora do método. Já o método Soma apenas lê os elementos, não precisa modificar a slice, então receiver de valor é suficiente e evita efeitos colaterais.