No artigo anterior, structs foram apresentadas como agrupamentos de dados. Métodos são o complemento natural — eles associam comportamento a esses tipos. Em Go, um método é simplesmente uma função com um argumento especial chamado receiver, que aparece entre a palavra-chave func e o nome do método.
Não existe uma palavra-chave especial como class ou object. Qualquer tipo definido no pacote atual pode ter métodos — não apenas structs, mas também tipos baseados em int, string ou qualquer outro tipo primitivo.
Declarando um método
A diferença sintática entre uma função e um método é o receiver:
package main
import "fmt"
type Retangulo struct {
Largura float64
Altura float64
}
// Função comum — recebe Retangulo como parâmetro
func areaFuncao(r Retangulo) float64 {
return r.Largura * r.Altura
}
// Método — receiver aparece antes do nome
func (r Retangulo) Area() float64 {
return r.Largura * r.Altura
}
func main() {
r := Retangulo{Largura: 5.0, Altura: 3.0}
fmt.Println(areaFuncao(r)) // 15 — chamada de função
fmt.Println(r.Area()) // 15 — chamada de método
}
O receiver (r Retangulo) declara que Area pertence ao tipo Retangulo. A variável r dentro do método é uma cópia do valor sobre o qual o método foi chamado — exatamente como um parâmetro de função normal.
Receiver por valor
Quando o receiver é declarado por valor, o método trabalha com uma cópia do valor original. Modificações feitas dentro do método não afetam o valor original:
type Contador struct {
valor int
}
func (c Contador) Incrementar() {
c.valor++ // modifica apenas a cópia local
}
func (c Contador) Valor() int {
return c.valor
}
func main() {
c := Contador{valor: 0}
c.Incrementar()
fmt.Println(c.Valor()) // 0 — não foi modificado
}
Receivers por valor são adequados quando o método apenas lê os dados da struct sem precisar modificá-los, e quando a struct é pequena o suficiente para que a cópia não seja custosa.
Receiver por ponteiro
Quando o receiver é declarado por ponteiro, o método recebe o endereço do valor original e pode modificá-lo diretamente:
func (c *Contador) Incrementar() {
c.valor++ // modifica o valor original
}
func main() {
c := Contador{valor: 0}
c.Incrementar()
c.Incrementar()
c.Incrementar()
fmt.Println(c.Valor()) // 3
}
A chamada c.Incrementar() funciona mesmo que c seja um valor, não um ponteiro. Go converte automaticamente para (&c).Incrementar() quando necessário — desde que c seja endereçável, ou seja, uma variável, não um valor literal ou retorno de função.
A regra de ouro: consistência no tipo de receiver
Go impõe uma convenção importante: se qualquer método de um tipo usa receiver por ponteiro, todos os métodos desse tipo devem usar receiver por ponteiro. Misturar os dois em um mesmo tipo causa confusão e problemas sutis com interfaces.
package main
import (
"fmt"
"math"
)
type Circulo struct {
Raio float64
}
func (c *Circulo) Area() float64 {
return math.Pi * c.Raio * c.Raio
}
func (c *Circulo) Perimetro() float64 {
return 2 * math.Pi * c.Raio
}
func (c *Circulo) Escalar(fator float64) {
c.Raio *= fator
}
func main() {
c := Circulo{Raio: 5.0}
fmt.Printf("Área: %.2f\n", c.Area())
fmt.Printf("Perímetro: %.2f\n", c.Perimetro())
c.Escalar(2.0)
fmt.Printf("Raio após escala: %.2f\n", c.Raio) // 10.00
fmt.Printf("Nova área: %.2f\n", c.Area())
}
Quando usar cada tipo de receiver
A decisão entre receiver por valor e por ponteiro segue critérios claros:
Use receiver por ponteiro quando:
O método precisa modificar o estado da struct. A struct é grande e a cópia seria cara em termos de memória e tempo. A struct contém campos que não devem ser copiados, como sync.Mutex.
Use receiver por valor quando:
O método apenas lê dados, sem modificar nada. O tipo é pequeno e simples — poucos campos de tipos básicos. A semântica de cópia é desejável, como em tipos que representam valores imutáveis.
Na prática, a maioria dos tipos Go com comportamento significativo usa receivers por ponteiro. A biblioteca padrão segue essa convenção extensivamente.
Métodos em tipos não-struct
Um detalhe poderoso do Go é que métodos podem ser definidos em qualquer tipo nomeado do pacote, não apenas em structs. Isso permite estender tipos simples com comportamento específico do domínio:
package main
import (
"fmt"
"strings"
)
type DiaDaSemana int
const (
Domingo DiaDaSemana = iota
Segunda
Terca
Quarta
Quinta
Sexta
Sabado
)
func (d DiaDaSemana) String() string {
nomes := []string{
"Domingo", "Segunda", "Terça",
"Quarta", "Quinta", "Sexta", "Sábado",
}
if d < Domingo || d > Sabado {
return "Desconhecido"
}
return nomes[d]
}
func (d DiaDaSemana) EFimDeSemana() bool {
return d == Domingo || d == Sabado
}
func main() {
hoje := Quarta
fmt.Println(hoje) // Quarta
fmt.Println(hoje.EFimDeSemana()) // false
fmt.Println(Sabado) // Sábado
fmt.Println(Sabado.EFimDeSemana()) // true
}
O método String() tem um papel especial: quando um tipo implementa String() string, o pacote fmt o chama automaticamente ao imprimir o valor. Esse é o primeiro contato com o conceito de interfaces, que será o tema do próximo artigo.
Encadeamento de métodos
Um padrão útil em Go é o method chaining — cada método retorna um ponteiro para o receptor, permitindo encadear chamadas:
package main
import "fmt"
type Consulta struct {
tabela string
condicoes []string
limite int
}
func (q *Consulta) Da(tabela string) *Consulta {
q.tabela = tabela
return q
}
func (q *Consulta) Onde(condicao string) *Consulta {
q.condicoes = append(q.condicoes, condicao)
return q
}
func (q *Consulta) Limite(n int) *Consulta {
q.limite = n
return q
}
func (q *Consulta) Construir() string {
sql := fmt.Sprintf("SELECT * FROM %s", q.tabela)
if len(q.condicoes) > 0 {
sql += " WHERE " + q.condicoes[0]
for _, c := range q.condicoes[1:] {
sql += " AND " + c
}
}
if q.limite > 0 {
sql += fmt.Sprintf(" LIMIT %d", q.limite)
}
return sql
}
func main() {
sql := (&Consulta{}).
Da("usuarios").
Onde("ativo = true").
Onde("idade >= 18").
Limite(10).
Construir()
fmt.Println(sql)
// SELECT * FROM usuarios WHERE ativo = true AND idade >= 18 LIMIT 10
}
O método String() e a interface Stringer
Como mencionado, implementar String() string faz com que o fmt use esse método automaticamente. Esse é o padrão idiomático para representação textual de tipos personalizados:
package main
import "fmt"
type Temperatura struct {
Celsius float64
}
func (t Temperatura) String() string {
return fmt.Sprintf("%.1f°C (%.1f°F)", t.Celsius, t.Celsius*9/5+32)
}
func main() {
t := Temperatura{Celsius: 36.6}
fmt.Println(t) // 36.6°C (97.9°F)
fmt.Printf("Temp: %v\n", t) // Temp: 36.6°C (97.9°F)
fmt.Printf("Temp: %s\n", t) // Temp: 36.6°C (97.9°F)
}
Promoted methods: métodos de campos embutidos
Quando uma struct embutida tem métodos, esses métodos também são promovidos ao tipo externo — o mesmo mecanismo que ocorre com campos:
package main
import "fmt"
type Motor struct {
Potencia int
}
func (m *Motor) Ligar() {
fmt.Printf("Motor de %d CV ligado.\n", m.Potencia)
}
func (m *Motor) Desligar() {
fmt.Println("Motor desligado.")
}
type Carro struct {
Modelo string
Motor // Motor embutido
}
func main() {
c := Carro{
Modelo: "Sedan",
Motor: Motor{Potencia: 150},
}
c.Ligar() // método promovido de Motor
fmt.Println("Modelo:", c.Modelo)
c.Desligar()
}
Se Carro definir seu próprio método Ligar(), ele sobrescreve o método promovido de Motor — o método do tipo externo tem precedência.
Exemplo completo: conta bancária
Um exemplo que consolida receivers por ponteiro, encapsulamento e o método String():
package main
import (
"errors"
"fmt"
)
type ContaBancaria struct {
titular string
saldo float64
}
func NovaConta(titular string, deposito float64) (*ContaBancaria, error) {
if deposito < 0 {
return nil, errors.New("depósito inicial não pode ser negativo")
}
return &ContaBancaria{titular: titular, saldo: deposito}, nil
}
func (c *ContaBancaria) Depositar(valor float64) error {
if valor <= 0 {
return errors.New("valor de depósito deve ser positivo")
}
c.saldo += valor
return nil
}
func (c *ContaBancaria) Sacar(valor float64) error {
if valor <= 0 {
return errors.New("valor de saque deve ser positivo")
}
if valor > c.saldo {
return errors.New("saldo insuficiente")
}
c.saldo -= valor
return nil
}
func (c *ContaBancaria) Saldo() float64 {
return c.saldo
}
func (c ContaBancaria) String() string {
return fmt.Sprintf("Conta[%s] Saldo: R$ %.2f", c.titular, c.saldo)
}
func main() {
conta, err := NovaConta("Ricardo", 1000.00)
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Println(conta)
conta.Depositar(500.00)
fmt.Println(conta)
if err := conta.Sacar(200.00); err != nil {
fmt.Println("Erro:", err)
}
fmt.Println(conta)
if err := conta.Sacar(5000.00); err != nil {
fmt.Println("Erro:", err) // Erro: saldo insuficiente
}
}
Resumo do que foi coberto
Este artigo apresentou métodos em Go com profundidade: a sintaxe do receiver, a diferença entre receiver por valor e por ponteiro, a regra de consistência, métodos em tipos não-struct, encadeamento de métodos, o método especial String() e a promoção de métodos via embedding. Esses conceitos preparam o terreno para o próximo artigo, onde interfaces transformam métodos em contratos polimórficos.
Referências e leituras complementares
-
Especificação da linguagem Go — Method declarations — Definição formal de métodos e receivers. https://go.dev/ref/spec#Method_declarations
-
A Tour of Go — Methods — Introdução interativa a métodos e receivers. https://go.dev/tour/methods/1
-
Go by Example: Methods — Exemplos práticos comentados de métodos. https://gobyexample.com/methods
-
Effective Go — Methods — Boas práticas para declaração e uso de métodos. https://go.dev/doc/effective_go#methods
-
Go FAQ: Should I define methods on values or pointers? — Resposta oficial à dúvida mais comum sobre receivers. https://go.dev/doc/faq#methods_on_values_or_pointers
-
Go by Example: Interfaces — Próximo passo natural após dominar métodos. https://gobyexample.com/interfaces