Métodos em Go: functions com receiver
Em Go, um método é uma função associada a um tipo (geralmente um struct). A diferença prática é que o método ganha um parâmetro especial chamado receiver, que aparece antes do nome do método.
type Counter struct { n int }
func (c Counter) Value() int {
return c.n
}
func (c *Counter) Inc() {
c.n++
}O receiver pode ser por valor (c Counter) ou por ponteiro (c *Counter). Essa escolha afeta mutação, custo de cópia e quais métodos ficam disponíveis em cada forma do tipo.
Receiver por valor: leitura, imutabilidade e cópias
Quando o receiver é por valor, o método recebe uma cópia do valor. Isso é ótimo para métodos que apenas consultam dados e não precisam modificar o estado.
type Point struct { X, Y float64 }
func (p Point) Norm() float64 {
return p.X*p.X + p.Y*p.Y
}Critérios comuns para preferir receiver por valor:
- Não há mutação do estado do objeto.
- O tipo é pequeno (cópia barata) e/ou imutável por convenção.
- Você quer que o método funcione naturalmente tanto em valores quanto em ponteiros (o compilador ajuda em várias situações, mas a escolha ainda importa para interfaces e consistência).
Receiver por ponteiro: mutação e evitar custo de cópia
Com receiver por ponteiro, o método opera sobre o mesmo objeto, permitindo alterar o estado e evitando copiar estruturas grandes.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
type Buffer struct {
data [1024]byte
used int
}
func (b *Buffer) Reset() {
b.used = 0
}Critérios comuns para preferir receiver por ponteiro:
- O método precisa modificar o receiver (mutação).
- O tipo é grande e copiar seria caro.
- Você quer consistência: se um método do tipo precisa ser por ponteiro, costuma-se padronizar a maioria dos métodos como ponteiro para evitar confusão e problemas com interfaces.
Consistência e o “conjunto de métodos” (method set)
Em Go, existe a ideia de conjunto de métodos disponível para um tipo e para o ponteiro desse tipo:
- Um valor
Ttem os métodos com receiver(t T). - Um ponteiro
*Ttem os métodos com receiver(t T)e(t *T).
Isso impacta interfaces: se uma interface exige um método que você definiu com receiver por ponteiro, então apenas *T satisfaz essa interface (não T).
type Resetter interface { Reset() }
type Thing struct{}
func (t *Thing) Reset() {}
func Use(r Resetter) {}
func demo() {
var v Thing
Use(&v) // ok
// Use(v) // não compila: Thing não tem método Reset() no method set de valor
}Interfaces em Go: pequenas, comportamentais e satisfação implícita
Uma interface define um conjunto de métodos (comportamentos). Em Go, interfaces são mais idiomáticas quando são pequenas e descrevem o que algo faz, não “o que algo é”.
type Stringer interface {
String() string
}Satisfação implícita
Um tipo satisfaz uma interface implicitamente ao implementar seus métodos. Não existe implements. Isso reduz acoplamento: você pode criar tipos que “passam a funcionar” com APIs existentes apenas por terem os métodos necessários.
type Notifier interface {
Notify(to, msg string) error
}
type Email struct{}
func (Email) Notify(to, msg string) error { return nil }
func Send(n Notifier, to, msg string) error {
return n.Notify(to, msg)
}Design idiomático: prefira interfaces no consumidor
Uma prática comum é: quem usa define a interface mínima que precisa, em vez de o produtor criar interfaces grandes e genéricas. Isso mantém o design flexível e facilita testes.
// O consumidor só precisa disso:
type Saver interface {
Save(key string, value []byte) error
}Composição de interfaces
Interfaces podem ser compostas a partir de outras, formando contratos maiores quando necessário, sem perder a granularidade.
type Reader interface {
Read(key string) ([]byte, error)
}
type Writer interface {
Write(key string, value []byte) error
}
type ReadWriter interface {
Reader
Writer
}Isso permite que funções aceitem o menor contrato possível:
func Load(r Reader, key string) ([]byte, error) {
return r.Read(key)
}
func Store(w Writer, key string, v []byte) error {
return w.Write(key, v)
}Como “testar implementações” de interface
Às vezes você quer garantir em tempo de compilação que um tipo implementa uma interface. Um padrão idiomático é a atribuição para o identificador em branco (_).
type Notifier interface {
Notify(to, msg string) error
}
type EmailNotifier struct{}
func (EmailNotifier) Notify(to, msg string) error { return nil }
var _ Notifier = (*EmailNotifier)(nil) // ou EmailNotifier{}, dependendo dos receiversSe a assinatura não bater (ou se o receiver estiver errado), o código não compila, e você descobre cedo.
Passo a passo prático: mini-projeto de notificações com duas implementações
Objetivo: criar uma interface pequena (Notifier) e duas implementações: uma que “envia” por e-mail (simulada) e outra que registra em memória (útil para testes). Depois, compor um serviço que depende apenas da interface.
1) Defina a interface orientada a comportamento
package notify
type Notifier interface {
Notify(to, msg string) error
}Repare que a interface é mínima: um único método, com um propósito claro.
2) Implemente a primeira estratégia: EmailNotifier (simulado)
Vamos usar receiver por valor porque o tipo não precisa manter estado mutável.
package notify
import "fmt"
type EmailNotifier struct {
From string
}
func (e EmailNotifier) Notify(to, msg string) error {
// Simulação: em um projeto real, aqui entraria SMTP/API.
fmt.Printf("EMAIL from=%s to=%s msg=%q\n", e.From, to, msg)
return nil
}3) Implemente a segunda estratégia: MemoryNotifier (mutável)
Agora precisamos armazenar as mensagens enviadas. Isso exige mutação, então usamos receiver por ponteiro.
package notify
type Sent struct {
To string
Msg string
}
type MemoryNotifier struct {
Sent []Sent
}
func (m *MemoryNotifier) Notify(to, msg string) error {
m.Sent = append(m.Sent, Sent{To: to, Msg: msg})
return nil
}Critério aplicado: mutação do estado interno (Sent) → receiver por ponteiro.
4) Garanta em compile-time que ambos satisfazem a interface
package notify
var _ Notifier = EmailNotifier{}
var _ Notifier = (*MemoryNotifier)(nil)Note a diferença: EmailNotifier{} satisfaz por valor; MemoryNotifier satisfaz apenas como ponteiro.
5) Crie um serviço que depende apenas da interface (polimorfismo)
Polimorfismo em Go aparece quando uma função/struct trabalha com uma interface e pode receber diferentes implementações.
package notify
type Service struct {
N Notifier
}
func (s Service) Welcome(to string) error {
return s.N.Notify(to, "Bem-vindo!")
}
func (s Service) PasswordReset(to string) error {
return s.N.Notify(to, "Aqui está seu link de redefinição")
}Service não sabe se está enviando e-mail ou gravando em memória. Ele só conhece o comportamento Notify.
6) Use o serviço com implementações diferentes
package main
import "seu/modulo/notify"
func main() {
s1 := notify.Service{N: notify.EmailNotifier{From: "noreply@site"}}
_ = s1.Welcome("ana@exemplo")
mem := ¬ify.MemoryNotifier{}
s2 := notify.Service{N: mem}
_ = s2.PasswordReset("bob@exemplo")
// mem.Sent agora contém o histórico (útil em testes)
}Testando o mini-projeto: foco em comportamento
Como o serviço depende de uma interface pequena, testar fica simples: use MemoryNotifier como dublê (test double) e verifique o que foi “enviado”.
package notify_test
import (
"testing"
"seu/modulo/notify"
)
func TestWelcomeSendsMessage(t *testing.T) {
mem := ¬ify.MemoryNotifier{}
svc := notify.Service{N: mem}
if err := svc.Welcome("ana@exemplo"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(mem.Sent) != 1 {
t.Fatalf("expected 1 message, got %d", len(mem.Sent))
}
if mem.Sent[0].To != "ana@exemplo" {
t.Fatalf("unexpected to: %s", mem.Sent[0].To)
}
if mem.Sent[0].Msg != "Bem-vindo!" {
t.Fatalf("unexpected msg: %q", mem.Sent[0].Msg)
}
}Checklist de escolha: receiver por valor vs ponteiro
| Critério | Prefira receiver por valor | Prefira receiver por ponteiro |
|---|---|---|
| Mutação do estado | Não | Sim |
| Custo de cópia | Tipo pequeno, cópia barata | Tipo grande, cópia cara |
| Consistência do tipo | Todos os métodos são naturalmente “read-only” | Alguns métodos precisam de ponteiro; padronize para evitar surpresas |
| Satisfação de interface | Quer que T satisfaça a interface | Ok exigir *T (ex.: precisa mutar) |
Interfaces pequenas: exemplos de refinamento
Se você perceber que uma interface está crescendo demais, considere quebrá-la em comportamentos menores e compor quando necessário.
// Evite (muito ampla):
type BigNotifier interface {
Notify(to, msg string) error
Validate(to string) error
Format(msg string) string
Stats() map[string]int
}
// Prefira (pequenas e compostas quando preciso):
type Notifier interface { Notify(to, msg string) error }
type Validator interface { Validate(to string) error }
type Formatter interface { Format(msg string) string }
type NotifierWithValidation interface {
Notifier
Validator
}