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