Métodos: funções associadas a tipos

1. O que são Métodos em Go

Em Go, um método é uma função especial que possui um receiver — um parâmetro adicional que vincula a função a um tipo específico. Enquanto funções comuns operam independentemente, métodos definem comportamentos associados diretamente a dados.

A sintaxe básica de um método é:

func (t Tipo) NomeDoMetodo(parametros) TipoRetorno {
    // corpo do método
}

A diferença fundamental entre função e método é que o método só pode ser chamado através de uma instância do tipo ao qual está associado. Vamos ver um exemplo prático:

package main

import "fmt"

type Retangulo struct {
    Largura, Altura float64
}

// Método Area() associado ao tipo Retangulo
func (r Retangulo) Area() float64 {
    return r.Largura * r.Altura
}

func main() {
    ret := Retangulo{Largura: 10, Altura: 5}
    fmt.Println("Área do retângulo:", ret.Area()) // 50
}

Aqui, Area() é um método porque possui o receiver (r Retangulo). Diferente de uma função, não precisamos passar o retângulo como argumento — chamamos diretamente ret.Area().

2. Declarando Métodos para Tipos Customizados

Métodos podem ser declarados para qualquer tipo definido no mesmo pacote, incluindo structs e tipos base.

type Nome string

func (n Nome) ToUpper() string {
    return strings.ToUpper(string(n))
}

type Pessoa struct {
    Nome string
    Idade int
}

func (p Pessoa) Saudacao() string {
    return "Olá, eu sou " + p.Nome
}

Restrição importante: métodos só podem ser declarados no mesmo pacote do tipo. Não é possível adicionar métodos a tipos de pacotes externos.

3. Receivers por Valor vs por Ponteiro

Go oferece duas formas de receiver: por valor e por ponteiro. A escolha impacta diretamente se o método pode modificar o receptor original.

type Conta struct {
    Saldo float64
}

// Receiver por valor: não modifica o original
func (c Conta) ExibirSaldo() float64 {
    return c.Saldo
}

// Receiver por ponteiro: permite modificar o original
func (c *Conta) Depositar(valor float64) {
    c.Saldo += valor
}

func main() {
    conta := Conta{Saldo: 1000}
    conta.Depositar(500)
    fmt.Println(conta.ExibirSaldo()) // 1500
}

Go resolve automaticamente chamadas: se você tem um valor e o método espera ponteiro (ou vice-versa), o compilador faz a conversão implícita. No entanto, a regra prática é: use ponteiro quando precisar modificar o receiver ou quando a struct for grande (evita cópia), e use valor para tipos pequenos e imutáveis.

4. Métodos com Ponteiros e Nil

É possível chamar métodos em ponteiros nil, mas isso requer cuidado para evitar panic:

type Lista struct {
    Valor int
    Prox  *Lista
}

func (l *Lista) Listar() {
    if l == nil {
        fmt.Println("Lista vazia")
        return
    }
    atual := l
    for atual != nil {
        fmt.Println(atual.Valor)
        atual = atual.Prox
    }
}

func main() {
    var lista *Lista // nil
    lista.Listar() // "Lista vazia" — seguro
}

A boa prática é sempre verificar if t == nil em métodos que podem receber nil, documentando esse comportamento.

5. Encadeamento de Métodos (Method Chaining)

Uma técnica poderosa é retornar o próprio receiver para permitir chamadas encadeadas, criando uma fluent interface:

type PessoaBuilder struct {
    Nome  string
    Idade int
}

func (p *PessoaBuilder) SetNome(nome string) *PessoaBuilder {
    p.Nome = nome
    return p
}

func (p *PessoaBuilder) SetIdade(idade int) *PessoaBuilder {
    p.Idade = idade
    return p
}

func (p *PessoaBuilder) Build() Pessoa {
    return Pessoa{Nome: p.Nome, Idade: p.Idade}
}

// Uso:
pessoa := new(PessoaBuilder).SetNome("João").SetIdade(30).Build()

Cuidado: se o receiver for por valor, o encadeamento não funcionará corretamente, pois cada chamada opera em uma cópia diferente.

6. Métodos e Interfaces

Em Go, interfaces são implementadas implicitamente. Um tipo implementa uma interface se possuir todos os métodos exigidos por ela:

type Stringer interface {
    String() string
}

type Pessoa struct {
    Nome string
}

func (p Pessoa) String() string {
    return "Pessoa: " + p.Nome
}

func main() {
    var s Stringer = Pessoa{"Maria"}
    fmt.Println(s.String()) // "Pessoa: Maria"
}

Interfaces exigem métodos, não campos. Isso permite polimorfismo e desacoplamento. Type assertion e type switch permitem verificar o tipo concreto por trás da interface:

switch v := s.(type) {
case Pessoa:
    fmt.Println("É uma pessoa:", v.Nome)
default:
    fmt.Println("Tipo desconhecido")
}

7. Métodos em Tipos Embutidos (Embedding)

Go não tem herança clássica, mas oferece embedding — um mecanismo onde um struct "herda" métodos de outro:

type Animal struct {
    Nome string
}

func (a Animal) Falar() string {
    return "Som genérico"
}

type Cachorro struct {
    Animal          // embedding
    Raca string
}

// Sobrescrita (shadowing) do método Falar
func (c Cachorro) Falar() string {
    return "Au au!"
}

func main() {
    c := Cachorro{
        Animal: Animal{Nome: "Rex"},
        Raca:   "Labrador",
    }
    fmt.Println(c.Falar())      // "Au au!" — método sobrescrito
    fmt.Println(c.Animal.Falar()) // "Som genérico" — acesso direto
}

Métodos do tipo embutido são promovidos automaticamente, permitindo acesso direto sem qualificação.

8. Boas Práticas e Armadilhas Comuns

Guia prático para receiver:
- Use ponteiro se o método modifica o receiver
- Use ponteiro se a struct for grande (evita cópia dispendiosa)
- Use valor para tipos pequenos e imutáveis (int, string, etc.)
- Seja consistente: se um método usa ponteiro, todos os métodos do tipo devem usar

Armadilhas comuns:

  1. Métodos que não acessam o receiver: isso geralmente indica que deveria ser uma função, não um método.
// Ruim: método que ignora o receiver
func (r Retangulo) CalcularPI() float64 {
    return 3.14159
}

// Melhor: função independente
func CalcularPI() float64 {
    return 3.14159
}
  1. Métodos muito longos: cada método deve ter uma única responsabilidade clara.

  2. Nomeação inconsistente: use nomes descritivos e consistentes (ex: GetNome, SetNome em vez de PegarNome, Nome).

  3. Esquecer nil check: sempre proteja métodos que podem receber ponteiros nil.

Métodos são uma das características mais elegantes de Go, permitindo associar comportamento a dados de forma clara e eficiente. Quando usados corretamente, tornam o código mais expressivo, seguro e fácil de manter.


Referências