Struct embedding no Go em vez de herança
1. Visão geral
Se você está acostumado com Java ou C#, a primeira pergunta ao escrever Go geralmente é: “extends cadê?”. Go não possui herança. Em vez disso, Go possui struct embedding: embutir um tipo em outra struct para reutilizar fields e methods.
Este artigo explica como embedding funciona, as diferenças em relação à herança, e as armadilhas comuns. Depois de ler, você será capaz de usar embedding para compartilhar comportamento, escrever decorators elegantes e evitar seus erros típicos.
2. Struct embedding básico
Embedding é uma declaração de um field apenas com tipo, sem nome:
package main
import "fmt"
type Address struct {
Street string
City string
}
func (a Address) FullAddress() string {
return a.Street + ", " + a.City
}
type Customer struct {
Name string
Address // campo embutido
}
func main() {
c := Customer{
Name: "Lan",
Address: Address{Street: "12 Lê Lợi", City: "Huế"},
}
fmt.Println(c.City) // field é promovido
fmt.Println(c.FullAddress()) // method é promovido
fmt.Println(c.Address.City) // acesso explícito
}
Huế
12 Lê Lợi, Huế
Huế
Análise:
Addressestá dentro deCustomersem nome de field. Go automaticamente define o nome do field comoAddress, que é o nome do tipo.- O field
Citye o methodFullAddress()deAddresssão promovidos paraCustomer. Por isso você pode escreverc.Cityem vez dec.Address.City. - Quando inicializa com composite literal, você ainda precisa usar o nome do field
Address:. Não há sintaxe para “preencher diretamente”CityemCustomer.
Em essência, Customer contém um Address. Esta é uma relação has-a (tem-um), apenas com sintaxe adicional fornecida pelo compilador. Customer não é um Address. Você não pode passar um Customer para uma função que espera um Address:
func printCity(a Address) { fmt.Println(a.City) }
// printCity(c) // compile error: cannot use c (type Customer) as Address
printCity(c.Address) // OK
3. “Sobrescrita” de method via shadowing
Se a struct externa declara um method com o mesmo nome do method do tipo embutido, o method da struct externa obscurece o método interno:
package main
import "fmt"
type Logger struct {
Prefix string
}
func (l Logger) Log(msg string) {
fmt.Println(l.Prefix + msg)
}
type OrderService struct {
Logger
}
func (s OrderService) Log(msg string) {
s.Logger.Log("[order] " + msg) // chama o method do tipo embutido
}
func main() {
s := OrderService{Logger: Logger{Prefix: "> "}}
s.Log("created #1001")
s.Logger.Log("raw message")
}
> [order] created #1001
> raw message
s.Log(...)chama o method deOrderService, porque tem precedência.s.Logger.Log(...)ainda consegue chamar o original. Isso é equivalente asuper.log(...)em Java.
À primeira vista parece override. Mas este é exatamente o lugar onde muitas pessoas tropeçam, como a próxima seção mostra.
4. A maior armadilha: sem virtual dispatch
Em Java, se a classe pai chama this.channel() e a classe filha faz override de channel(), a versão da classe filha será executada. Go não faz isso:
package main
import "fmt"
type Notifier struct{}
func (n Notifier) Channel() string { return "email" }
func (n Notifier) Send(msg string) string {
return "[" + n.Channel() + "] " + msg
}
type SMSNotifier struct {
Notifier
}
func (s SMSNotifier) Channel() string { return "sms" }
func main() {
s := SMSNotifier{}
fmt.Println(s.Channel())
fmt.Println(s.Send("Đơn hàng đã giao"))
}
sms
[email] Đơn hàng đã giao
s.Send(...) na verdade é s.Notifier.Send(...). Dentro de Send, o receiver n tem tipo Notifier. Ele não sabe que está dentro de um SMSNotifier, então n.Channel() sempre retorna "email".
Essa é a diferença essencial. O tipo embutido não tem referência de volta para a struct que o contém. O padrão “template method” ao estilo Java não se aplica diretamente.
4.1. Solução: extrair a parte variável em interface
Se você precisa de comportamento que possa variar, passe-o explicitamente:
package main
import "fmt"
type Channel interface {
Name() string
}
type Email struct{}
func (Email) Name() string { return "email" }
type SMS struct{}
func (SMS) Name() string { return "sms" }
type Notifier struct {
Channel Channel
}
func (n Notifier) Send(msg string) string {
return "[" + n.Channel.Name() + "] " + msg
}
func main() {
fmt.Println(Notifier{Channel: SMS{}}.Send("Đơn hàng đã giao"))
fmt.Println(Notifier{Channel: Email{}}.Send("Hóa đơn tháng 10"))
}
[sms] Đơn hàng đã giao
[email] Hóa đơn tháng 10
Agora a parte variável (Channel) é uma dependency explícita. Este é o jeito mais “Go” de fazer, em vez de tentar simular override.
5. Embedding e interface
Interfaces em Go são satisfeitas implicitamente. Methods promovidos também são contabilizados no method set da struct externa. Portanto, embedding ajuda a struct satisfazer uma interface sem ter de reescrever o method:
package main
import "fmt"
type Describer interface {
FullAddress() string
}
type Address struct{ Street, City string }
func (a Address) FullAddress() string { return a.Street + ", " + a.City }
type Warehouse struct {
Code string
Address
}
func describe(d Describer) { fmt.Println(d.FullAddress()) }
func main() {
w := Warehouse{Code: "HN-01", Address: Address{"KCN Thăng Long", "Hà Nội"}}
describe(w) // Warehouse satisfaz Describer graças ao method promovido
}
KCN Thăng Long, Hà Nội
5.1. Nota sobre receiver com ponteiro
A regra do method set ao embutir é como segue:
Declarado em S |
Method set de S |
Method set de *S |
|---|---|---|
embutir T |
method receiver T |
method receiver T e *T |
embutir *T |
method receiver T e *T |
method receiver T e *T |
Por exemplo, se Counter possui func (c *Counter) Inc() e você embutir Counter (não um ponteiro) em Stats, apenas *Stats satisfaz uma interface que tem Inc(). O valor Stats não. Erros de compilação do tipo “Stats does not implement Incrementer (method Inc has pointer receiver)” geralmente vêm disso.
6. Embutir interface em struct: decorator elegante
Você pode embutir uma interface em uma struct. A struct terá automaticamente todos os methods da interface, cada method delegado para o valor interno. Você só precisa sobrescrever os methods que quer mudar:
package payment
import "fmt"
type PaymentService interface {
Charge(orderID string, amount int) error
Refund(orderID string) error
}
type LoggingPayment struct {
PaymentService // delega todos os methods
Logs []string
}
func (l *LoggingPayment) Charge(orderID string, amount int) error {
l.Logs = append(l.Logs, fmt.Sprintf("charge %s: %d", orderID, amount))
return l.PaymentService.Charge(orderID, amount)
}
LoggingPaymentsó reescreveCharge.Refundé delegado automaticamente para oPaymentServiceinterno.*LoggingPaymentsatisfazPaymentService, então você pode envolver qualquer implementação sem que o chamador precise saber.
Quando uma interface tem dez methods e você só precisa interceptar um, essa técnica economiza muito código repetitivo.
Nota: Se o field de interface embutido é
nil, o código ainda compila normalmente. Mas chamar um method que não foi sobrescrito (ex:Refund) causará um panic de nil pointer dereference em runtime. Sempre inicialize através de um construtor, ex:NewLoggingPayment(inner PaymentService).
7. Testando comportamento delegado
Go usa o package testing padrão em vez de JUnit. O teste abaixo verifica duas coisas: Charge é registrado em log e ainda chama o serviço original, enquanto Refund é delegado sem logging.
package payment
import "testing"
type fakePayment struct {
charged []string
refunded []string
}
func (f *fakePayment) Charge(orderID string, amount int) error {
f.charged = append(f.charged, orderID)
return nil
}
func (f *fakePayment) Refund(orderID string) error {
f.refunded = append(f.refunded, orderID)
return nil
}
func TestLoggingPayment(t *testing.T) {
inner := &fakePayment{}
var svc PaymentService = &LoggingPayment{PaymentService: inner}
if err := svc.Charge("ORD-1", 250000); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := svc.Refund("ORD-1"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
lp := svc.(*LoggingPayment)
if len(lp.Logs) != 1 || lp.Logs[0] != "charge ORD-1: 250000" {
t.Errorf("logs = %v, want [charge ORD-1: 250000]", lp.Logs)
}
if len(inner.charged) != 1 || len(inner.refunded) != 1 {
t.Errorf("inner not called: charged=%v refunded=%v", inner.charged, inner.refunded)
}
}
Execute o teste:
go test ./...
ok example.com/shop/payment 0.002s
A linha var svc PaymentService = &LoggingPayment{...} também é uma verificação em tempo de compilação: se *LoggingPayment não satisfaz a interface, o teste não será compilado. No código de produção, você pode usar a forma conhecida var _ PaymentService = (*LoggingPayment)(nil) para o mesmo propósito.
8. Conflitos de nomes e erros comuns
8.1. Ambiguous selector
Quando dois tipos embutidos na mesma profundidade compartilham o mesmo nome de field ou method, acessar através da struct externa resultará em erro de compilação:
type Billing struct{ Email string }
type Shipping struct{ Email string }
type Order struct {
Billing
Shipping
}
func main() {
o := Order{}
// fmt.Println(o.Email) // compile error: ambiguous selector o.Email
fmt.Println(o.Billing.Email) // OK
}
A struct Order ainda é válida. O erro só aparece quando você acessa o.Email. A forma de lidar é chamar explicitamente, ou declarar Email diretamente em Order para obscurecer ambos.
8.2. Expor API acidentalmente: exemplo com sync.Mutex
type Inventory struct {
sync.Mutex
stock map[string]int
}
Este código funciona. Mas Lock() e Unlock() agora são methods públicos de Inventory, porque o nome do field embutido Mutex é exportado. Código fora do package pode travar seu inventário. Melhor escrever:
type Inventory struct {
mu sync.Mutex
stock map[string]int
}
A regra geral: embedding traz todos os methods do tipo embutido para a API pública. Se você não quer expô-los, use um field com nome.
8.3. Embutir ponteiro como nil
Embutir *Address em vez de Address significa que o valor zero da struct contém um ponteiro nil. Quando isso acontece, c.City causará panic. Só embutir ponteiro quando você realmente precisa compartilhar a instância ou precisa do method set de *T, e lembre-se de inicializá-lo.
9. Quando usar e quando não usar embedding
Usar quando:
- Você quer que a struct externa realmente exponha todo o comportamento do tipo embutido, ex: embutir
http.Handlerouio.Reader. - Você está escrevendo um decorator ou wrapper que muda apenas alguns methods de uma interface grande.
- Você está agrupando fields comuns como
Audit{CreatedAt, UpdatedAt}em várias entidades.
Não usar quando:
- Você está tentando recriar uma árvore de herança multilevel com “classe pai chamando método da classe filha”. Use interface e dependency explícita (seção 4.1).
- O tipo embutido tem methods que você não quer expor (seção 8.2).
- Você só precisa de um ou dois methods do outro tipo. Um field com nome mais alguns methods de delegação escritos manualmente será mais claro.
Nota: Embedding não cria uma relação is-a. Se você precisa de polimorfismo, defina uma interface e deixe vários tipos satisfazê-la. Não conte com “struct filha é struct pai”.
10. Conclusão
Struct embedding é a ferramenta de composição do Go, acompanhada por um pouco de açúcar sintático: fields e methods do tipo embutido são promovidos para a struct externa. Ajuda você a reutilizar código, satisfazer interfaces e escrever decorators muito elegantes.
No entanto, embedding não é herança. Não há virtual dispatch, o tipo embutido não sabe da struct que o contém, e cada method promovido vira parte da sua API pública. Lembre-se de três coisas: use interface para a parte comportamental que varia, inicialize completamente os fields de interface ou ponteiros embutidos, e mude para fields com nome quando não quer expor methods para fora.