Métodos
Esta aula aborda métodos em Go, explicando como definir e usar métodos com receivers de valor e ponteiro. Serão discutidos quando usar cada tipo de receiver, como criar métodos em tipos próprios, com exemplos práticos.
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
- A Tour of Go: Methods
- Effective Go: Methods
- Go by Example: Methods
- Package builtin (para entender tipos nativos)
- DigitalOcean: Defining Methods in Go
Exercícios
Crie um tipo
Circulocom campoRaio float64. Defina um métodoArea() float64que 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()) }Para o tipo
Circulodo exercício anterior, adicione um métodoEscalar(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) // 10Crie um tipo
Listabaseado em[]int. Defina um métodoSoma() intque 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 }Para o tipo
Listaacima, adicione um métodoAdicionar(valor int)que adiciona um elemento ao final da slice. Use receiver de ponteiro. (Dica: useappend)✓ 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]Explique por que o método
Adicionardo exercício anterior precisa de um receiver de ponteiro, enquanto o métodoSomapode usar receiver de valor.✓ Resposta:O métodoAdicionarmodifica 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étodoSomaapenas lê os elementos, não precisa modificar a slice, então receiver de valor é suficiente e evita efeitos colaterais.