Golang

Métodos em Structs: receivers por valor e por ponteiro Já leu

9 min de leitura

Métodos em Structs: receivers por valor e por ponteiro
No artigo anterior, structs foram apresentadas como agrupamentos de dados. Métodos são o complemento natural — eles associam comportamento a

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

Comentários

Mais em Golang

Introdução ao Go: história, filosofia e os criadores da linguagem
Introdução ao Go: história, filosofia e os criadores da linguagem

Talvez voc&ecirc; se pergunte:&nbsp;Por que mais uma linguagem de programa&cc...

Estruturas de controle: if, for e switch
Estruturas de controle: if, for e switch

Todo programa &uacute;til precisa tomar decis&otilde;es e repetir opera&ccedi...

Funções: declaração, múltiplos retornos e variádicas
Funções: declaração, múltiplos retornos e variádicas

Em Go, fun&ccedil;&otilde;es s&atilde;o cidad&atilde;s de primeira classe. El...