Métodos
Nesta aula, você aprenderá sobre métodos em Go, incluindo a diferença entre receivers por valor e por ponteiro, quando usar cada um, e como definir métodos em tipos próprios. Serão apresentados exemplos práticos para consolidar o entendimento.
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
- Effective Go - Methods
- Go Specification - Method declarations
- A Tour of Go - Methods
- DigitalOcean - Defining Methods in Go
- Medium - 5 simple tips for using methods in Go
Exercícios
- Crie um tipo
Circulocom campoRaio(float64). Implemente um métodoArea()que retorna a área do círculo (π * raio²). Use receiver por valor. - Adicione um método
Escalar(fator float64)ao tipoCirculoque multiplique o raio pelo fator. Use receiver por ponteiro. - Crie um tipo
ListaNumerosbaseado em[]int. Implemente um métodoSoma()que retorna a soma de todos os elementos. - Explique por que o método
Somado exercício anterior deve usar receiver por valor e não por ponteiro. - Escreva um programa que defina um tipo
Pontocom camposXeY(float64). Implemente um métodoDistanciaOrigem()que retorna a distância do ponto até a origem (0,0). Use receiver por valor. Depois, implemente um métodoMove(dx, dy float64)que desloca o ponto. Use receiver por ponteiro. Teste no main.
type Circulo struct {
Raio float64
}
func (c Circulo) Area() float64 {
return math.Pi * c.Raio * c.Raio
}
func (c *Circulo) Escalar(fator float64) {
c.Raio *= fator
}
type ListaNumeros []int
func (l ListaNumeros) Soma() int {
total := 0
for _, v := range l {
total += v
}
return total
}
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.
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}
}