Métodos em Go são funções associadas a um tipo específico. Eles permitem que você defina comportamentos para seus tipos, similar a métodos em classes de linguagens orientadas a objetos, mas com uma abordagem mais simples e flexível. Em Go, um método é declarado com um parâmetro especial chamado receiver, que aparece entre a palavra-chave func e o nome do método.

O receiver pode ser do tipo valor ou ponteiro, e essa escolha afeta como o método pode modificar o valor do receptor e como ele é chamado. Vamos explorar esses conceitos em detalhes.

Receivers (valor vs ponteiro)

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

Para declarar um método com receiver por valor, use o tipo sem o asterisco. Para receiver por ponteiro, use o tipo com * antes do nome do tipo. Exemplo:

type Pessoa struct {
    Nome string
    Idade int
}

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

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

Quando usar cada um

A escolha entre receiver por valor e por ponteiro depende de vários fatores:

  • Modificação: Se o método precisa modificar o receptor, use receiver por ponteiro. Caso contrário, valor é suficiente.
  • Tamanho do tipo: Para tipos grandes (como structs com muitos campos), o receiver por valor pode ser caro em termos de cópia. Nesse caso, prefira ponteiro.
  • Consistência: Se um tipo tem algum método com receiver por ponteiro, é comum que todos os métodos desse tipo usem ponteiro para consistência.
  • Tipos básicos: Para tipos como int, string, etc., geralmente use valor, a menos que precise modificar.

Exemplo prático:

type Conta struct {
    saldo float64
}

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

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

func (c *Conta) Sacar(valor float64) bool {
    if c.saldo >= valor {
        c.saldo -= valor
        return true
    }
    return false
}

Métodos em tipos próprios

Você pode definir métodos em qualquer tipo definido no seu pacote, não apenas structs. Isso inclui tipos baseados em tipos primitivos, como type MeuInteiro int, ou até tipos de slice, map, etc.

Exemplo com tipo baseado em string:

type Celsius float64

func (c Celsius) String() string {
    return fmt.Sprintf("%.2f°C", c)
}

func (c Celsius) ToFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

type Fahrenheit float64

func (f Fahrenheit) String() string {
    return fmt.Sprintf("%.2f°F", f)
}

Exemplos

Vamos ver um exemplo completo que usa métodos com receivers valor e ponteiro em um tipo struct:

package main

import "fmt"

type Retangulo struct {
    Largura, Altura float64
}

// Receiver por valor
func (r Retangulo) Area() float64 {
    return r.Largura * r.Altura
}

// Receiver por ponteiro
func (r *Retangulo) Redimensionar(fator float64) {
    r.Largura *= fator
    r.Altura *= fator
}

func main() {
    ret := Retangulo{Largura: 10, Altura: 5}
    fmt.Println("Área:", ret.Area()) // 50
    ret.Redimensionar(2)
    fmt.Println("Nova largura:", ret.Largura) // 20
    fmt.Println("Nova altura:", ret.Altura)   // 10
    fmt.Println("Nova área:", ret.Area())     // 200
}

Note que mesmo chamando um método com receiver por ponteiro em uma variável do tipo valor, Go automaticamente converte para ponteiro, e vice-versa. Isso torna o código mais flexível.

Boas práticas

  • Se um método não modifica o receptor, use receiver por valor, a menos que o tipo seja grande.
  • Se você não tem certeza, comece com receiver por ponteiro para evitar cópias desnecessárias.
  • Para tipos que representam algo mutável (como uma conta bancária), prefira ponteiro.
  • Evite misturar receivers valor e ponteiro no mesmo tipo, a menos que haja uma boa razão.

Observações finais

Métodos são uma parte fundamental da programação em Go, permitindo que você associe comportamentos a tipos de forma limpa e eficiente. Entender a diferença entre receivers valor e ponteiro é crucial para escrever código correto e performático.

Referências

Exercícios

  1. Crie um tipo Circulo com campo Raio (float64). Implemente um método Area() que retorna a área do círculo (π * raio²). Use receiver por valor.
  2. ✓ Resposta:
    type Circulo struct {
        Raio float64
    }
    
    func (c Circulo) Area() float64 {
        return math.Pi * c.Raio * c.Raio
    }
  3. Adicione um método Escalar(fator float64) ao tipo Circulo que multiplique o raio pelo fator. Use receiver por ponteiro.
  4. ✓ Resposta:
    func (c *Circulo) Escalar(fator float64) {
        c.Raio *= fator
    }
  5. Crie um tipo ListaNumeros baseado em []int. Implemente um método Soma() que retorna a soma de todos os elementos.
  6. ✓ Resposta:
    type ListaNumeros []int
    
    func (l ListaNumeros) Soma() int {
        total := 0
        for _, v := range l {
            total += v
        }
        return total
    }
  7. Explique por que o método Soma do exercício anterior deve usar receiver por valor e não por ponteiro.
  8. ✓ Resposta:

    O método Soma não modifica a lista, apenas a lê. Usar receiver por valor evita a necessidade de alocar um ponteiro e é mais simples. Além disso, slices são pequenas estruturas (contêm ponteiro para o array subjacente, comprimento e capacidade), então copiá-las é barato.

  9. Escreva um programa que defina um tipo Ponto com campos X e Y (float64). Implemente um método DistanciaOrigem() que retorna a distância do ponto até a origem (0,0). Use receiver por valor. Depois, implemente um método Move(dx, dy float64) que desloca o ponto. Use receiver por ponteiro. Teste no main.
  10. ✓ Resposta:
    package main
    
    import (
        "fmt"
        "math"
    )
    
    type Ponto struct {
        X, Y float64
    }
    
    func (p Ponto) DistanciaOrigem() float64 {
        return math.Sqrt(p.X*p.X + p.Y*p.Y)
    }
    
    func (p *Ponto) Move(dx, dy float64) {
        p.X += dx
        p.Y += dy
    }
    
    func main() {
        p := Ponto{3, 4}
        fmt.Println("Distância da origem:", p.DistanciaOrigem()) // 5
        p.Move(1, -1)
        fmt.Println("Novo ponto:", p) // {4, 3}
    }