Embedding de interfaces e structs
1. Fundamentos do Embedding em Go
Go não possui herança tradicional como linguagens orientadas a objeto. Em vez disso, oferece composição via embedding — um mecanismo onde uma struct ou interface é "embutida" anonimamente em outra, promovendo seus campos e métodos para o tipo pai.
A diferença fundamental é que, enquanto herança cria uma relação hierárquica rígida ("é um"), embedding estabelece uma relação de composição ("tem um") com delegação automática. Isso resulta em código mais flexível e com menor acoplamento.
A sintaxe básica é declarar um tipo sem nome de campo:
type Person struct {
Name string
Age int
}
type User struct {
Person // embedding anônimo
Email string
}
O comportamento de "promoção" faz com que campos e métodos de Person sejam acessíveis diretamente em User, como se fossem declarados ali.
2. Embedding de Structs: Composição Prática
Com embedding, o acesso a campos e métodos da struct embutida é direto:
u := User{
Person: Person{Name: "Alice", Age: 30},
Email: "alice@example.com",
}
fmt.Println(u.Name) // Acesso direto, sem u.Person.Name
fmt.Println(u.Age) // Promovido automaticamente
Métodos também são promovidos. Se Person tiver um método Greet(), ele estará disponível em User:
func (p Person) Greet() string {
return "Olá, eu sou " + p.Name
}
func main() {
u := User{Person: Person{Name: "Bob", Age: 25}, Email: "bob@example.com"}
fmt.Println(u.Greet()) // "Olá, eu sou Bob"
}
Para sobrescrever um método, basta declarar um método com o mesmo nome no tipo pai:
func (u User) Greet() string {
return "Usuário: " + u.Name
}
Nesse caso, u.Greet() chama o método de User, não o de Person. Para acessar o método original, use u.Person.Greet().
Exemplo real: reuso de código em sistemas de domínio:
type Address struct {
Street string
City string
Zip string
}
func (a Address) FullAddress() string {
return a.Street + ", " + a.City + " - " + a.Zip
}
type Company struct {
Address
Name string
CNPJ string
}
type Customer struct {
Address
Name string
Phone string
}
Ambos Company e Customer herdam o método FullAddress() sem duplicação de código.
3. Embedding de Interfaces: Polimorfismo por Composição
Interfaces também podem ser embutidas em outras interfaces, promovendo automaticamente seus métodos:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
ReadWriter agora exige que qualquer tipo implemente tanto Read quanto Write. Isso é mais expressivo e modular do que declarar todos os métodos em uma única interface.
O embedding de interfaces permite construir hierarquias planas e reutilizáveis:
type Closer interface {
Close() error
}
type ReadCloser interface {
Reader
Closer
}
type WriteCloser interface {
Writer
Closer
}
4. Embedding de Interfaces em Structs: Injeção de Comportamento
Quando uma interface é embutida em uma struct, ela define um contrato que pode ser preenchido por qualquer implementação concreta:
type Logger interface {
Log(message string)
}
type Server struct {
Logger // interface embutida
Address string
}
func (s Server) Start() {
s.Log("Servidor iniciado em " + s.Address)
}
Isso é extremamente útil para testes. Podemos injetar mocks facilmente:
type MockLogger struct {
messages []string
}
func (m *MockLogger) Log(msg string) {
m.messages = append(m.messages, msg)
}
func TestServerStart(t *testing.T) {
mock := &MockLogger{}
server := Server{
Logger: mock,
Address: ":8080",
}
server.Start()
if len(mock.messages) != 1 {
t.Errorf("esperava 1 mensagem, obteve %d", len(mock.messages))
}
}
Para produção, basta trocar por uma implementação real:
type ProductionLogger struct{}
func (p ProductionLogger) Log(msg string) {
fmt.Println("[LOG]", msg)
}
5. Conflitos e Armadilhas Comuns
Colisão de nomes: se duas structs embutidas possuem campos com o mesmo nome, o compilador gera erro:
type A struct { X int }
type B struct { X int }
type C struct {
A
B
}
// Erro: ambiguous selector c.X
A solução é acessar explicitamente: c.A.X ou c.B.X.
Chamadas ambíguas: o mesmo vale para métodos promovidos. Go resolve pela ordem de declaração — o método mais próximo (no tipo pai) tem prioridade.
Ponteiros vs valores: usar ponteiros no embedding evita cópias desnecessárias e permite mutação:
type Service struct {
*Logger // ponteiro para interface
}
Cuidado com nil pointer dereference se a interface embutida não for inicializada.
6. Boas Práticas e Padrões de Design
- Prefira embedding a herança: você ganha flexibilidade para compor comportamentos sem rigidez hierárquica.
- Use embedding de interfaces para contratos: defina interfaces pequenas e coesas, depois componha-as.
- Combine structs e interfaces: structs com interfaces embutidas permitem injeção de dependência natural.
- Evite interfaces muito grandes: uma interface com muitos métodos quebra o princípio da segregação de interfaces (ISP).
- Cuidado com dependências ocultas: embedding pode esconder dependências. Documente claramente o que está sendo embutido.
7. Exemplo Integrado: Sistema de Notificações
Vamos construir um sistema de notificações que demonstra embedding de interfaces e structs:
// Interfaces base
type Notifier interface {
Send(to string, message string) error
}
type Logger interface {
Log(message string)
}
// Struct base com funcionalidade comum
type BaseService struct {
Logger
}
func (b BaseService) Notify(to string, message string) error {
b.Log("Enviando notificação para " + to)
return nil
}
// Implementações concretas de Notifier
type EmailNotifier struct {
BaseService
SMTPHost string
}
func (e EmailNotifier) Send(to string, message string) error {
e.Log("Email para " + to + ": " + message)
// Lógica real de envio...
return nil
}
type SMSNotifier struct {
BaseService
APIKey string
}
func (s SMSNotifier) Send(to string, message string) error {
s.Log("SMS para " + to + ": " + message)
// Lógica real de envio...
return nil
}
// Serviço de notificação composto
type NotificationService struct {
Notifier // interface embutida
Logger // interface embutida
}
func (ns NotificationService) SendWelcomeEmail(userEmail string) {
ns.Log("Preparando email de boas-vindas")
ns.Notifier.Send(userEmail, "Bem-vindo ao sistema!")
}
// Mock para testes
type MockNotifier struct {
SentMessages []string
}
func (m *MockNotifier) Send(to string, message string) error {
m.SentMessages = append(m.SentMessages, to+": "+message)
return nil
}
// Teste unitário
func TestNotificationService(t *testing.T) {
mockNotifier := &MockNotifier{}
mockLogger := &MockLogger{}
service := NotificationService{
Notifier: mockNotifier,
Logger: mockLogger,
}
service.SendWelcomeEmail("teste@example.com")
if len(mockNotifier.SentMessages) != 1 {
t.Error("notificação não foi enviada")
}
}
Esse exemplo mostra como embedding de interfaces e structs permite:
- Reuso de comportamento (BaseService com Logger)
- Polimorfismo flexível (Notifier pode ser EmailNotifier ou SMSNotifier)
- Testabilidade com mocks (substituição de dependências)
O embedding em Go oferece uma alternativa poderosa à herança tradicional, promovendo composição, reuso e baixo acoplamento — pilares do design de software moderno.
Referências
- Effective Go: Embedding — Documentação oficial explicando o conceito de embedding em Go com exemplos práticos.
- Go by Example: Struct Embedding — Tutorial interativo demonstrando embedding de structs com código executável.
- Go Specification: Struct types — Especificação técnica detalhada sobre struct types e campos anônimos.
- Dave Cheney: Composition over Inheritance in Go — Artigo aprofundado comparando composição (embedding) com herança tradicional.
- The Go Blog: Interfaces in Go — Post oficial sobre interfaces, incluindo embedding e polimorfismo por composição.
- Practical Go: Embedding for Code Reuse — Guia prático com exemplos reais de reuso de código via embedding.