Em Go, não existe herança no sentido clássico de orientação a objetos. Em vez disso, a linguagem promove a composição como principal mecanismo de reutilização de código. O embedding (ou incorporação) é a ferramenta que permite que um tipo reuse campos e métodos de outro tipo de forma simples e elegante. Nesta aula, vamos explorar como funciona o embedding de structs e interfaces, como a promoção de métodos ocorre automaticamente e por que a composição é preferível à herança em Go.

Entender embedding é fundamental para projetar sistemas modulares e flexíveis. Diferente de herança, que cria uma hierarquia rígida, a composição permite montar comportamentos combinando tipos independentes. Vamos ver na prática como isso se traduz em código.

Embedding de structs

O embedding de structs permite que uma struct inclua outra struct anonimamente, ou seja, sem nome de campo. Isso faz com que os campos e métodos da struct embutida sejam promovidos para a struct externa, podendo ser acessados diretamente.

Por exemplo, considere uma struct Endereco com campos Rua e Cidade. Se criarmos uma struct Pessoa que embute Endereco, poderemos acessar Rua e Cidade diretamente em uma variável do tipo Pessoa, como se fossem campos próprios. Isso facilita a composição de tipos mais complexos.

package main

import "fmt"

type Endereco struct {
    Rua    string
    Cidade string
}

type Pessoa struct {
    Nome string
    Idade int
    Endereco // embedding anônimo
}

func main() {
    p := Pessoa{
        Nome:  "João",
        Idade: 30,
        Endereco: Endereco{
            Rua:    "Rua A",
            Cidade: "São Paulo",
        },
    }
    fmt.Println(p.Nome)   // João
    fmt.Println(p.Rua)    // Rua A (promovido)
    fmt.Println(p.Cidade) // São Paulo (promovido)
}

No exemplo acima, Endereco é embutido anonimamente. Isso significa que podemos acessar p.Rua diretamente, sem precisar de p.Endereco.Rua. No entanto, ainda podemos acessar o campo embutido explicitamente se houver ambiguidade ou por clareza.

Embedding de interfaces

Assim como structs, interfaces também podem ser embutidas em outras interfaces. Isso permite compor interfaces maiores a partir de interfaces menores, promovendo os métodos das interfaces embutidas.

Por exemplo, se tivermos uma interface Leitor com método Ler() e uma interface Escritor com método Escrever(), podemos criar uma interface LeitorEscritor que embute ambas. Qualquer tipo que implemente Ler() e Escrever() automaticamente implementa LeitorEscritor.

package main

import "fmt"

type Leitor interface {
    Ler() string
}

type Escritor interface {
    Escrever(dados string)
}

type LeitorEscritor interface {
    Leitor
    Escritor
}

type Arquivo struct {
    conteudo string
}

func (a *Arquivo) Ler() string {
    return a.conteudo
}

func (a *Arquivo) Escrever(dados string) {
    a.conteudo = dados
}

func main() {
    var le LeitorEscritor = &Arquivo{}
    le.Escrever("Olá, mundo!")
    fmt.Println(le.Ler()) // Olá, mundo!
}

No código acima, LeitorEscritor embute Leitor e Escritor. A struct Arquivo implementa ambos os métodos, portanto satisfaz a interface composta. Isso é muito útil para definir contratos de forma modular.

Promoção de métodos

Quando um tipo é embutido, todos os seus métodos são promovidos para o tipo externo. Isso significa que o tipo externo pode chamar métodos do tipo embutido diretamente, como se fossem seus próprios métodos.

Se o tipo externo definir um método com o mesmo nome de um método promovido, o método do tipo externo terá precedência (sombreamento). No entanto, ainda é possível chamar o método do tipo embutido explicitamente.

package main

import "fmt"

type Animal struct {
    Nome string
}

func (a Animal) Falar() string {
    return "..."
}

type Cachorro struct {
    Animal // embedding
}

func (c Cachorro) Falar() string {
    return "Au au!"
}

func main() {
    c := Cachorro{Animal: Animal{Nome: "Rex"}}
    fmt.Println(c.Falar())      // Au au! (método do Cachorro)
    fmt.Println(c.Animal.Falar()) // ... (método do Animal)
}

Note que c.Falar() chama o método de Cachorro, pois ele sombreia o método promovido de Animal. Para acessar o método original, usamos c.Animal.Falar(). A promoção de métodos é uma característica poderosa que permite reutilizar comportamentos sem herança.

vs herança

Herança é um conceito de orientação a objetos onde uma classe derivada herda campos e métodos de uma classe base, formando uma hierarquia. Em Go, não há herança; a composição é o mecanismo preferido. A composição oferece várias vantagens sobre herança:

  • Flexibilidade: Com composição, você pode combinar comportamentos de forma mais dinâmica, sem amarras hierárquicas.
  • Evita problemas de diamante: Herança múltipla pode causar ambiguidades; composição resolve isso claramente.
  • Menos acoplamento: Tipos compostos são independentes e podem ser reutilizados em diferentes contextos.
  • Facilidade de teste: Componentes pequenos e focados são mais fáceis de testar isoladamente.

Enquanto herança estabelece um relacionamento “é um” (ex.: um cachorro é um animal), composição estabelece “tem um” (ex.: um cachorro tem um animal). Go incentiva a pensar em termos de comportamentos (interfaces) e composição de tipos, resultando em código mais modular e de fácil manutenção.

Por exemplo, em vez de criar uma hierarquia de classes para diferentes tipos de funcionários, podemos compor um tipo Funcionario com campos como Pessoa e Cargo, e usar interfaces para comportamentos como Trabalhar(). Isso é mais natural em Go.

Boas práticas

  • Prefira composição a herança sempre que possível. Use embedding para reutilizar campos e métodos, mas evite criar hierarquias profundas.
  • Quando embutir um tipo, lembre-se de que os métodos promovidos podem ser sobrescritos. Use isso com cuidado para não causar confusão.
  • Para interfaces, o embedding é uma forma elegante de compor contratos. Prefira interfaces pequenas e focadas (princípio da segregação de interfaces).
  • Evite embutir tipos apenas para acessar campos privados; isso pode quebrar o encapsulamento.

Referências

Exercícios

  1. Crie uma struct Motor com campo Potencia (int) e método Ligar() que imprime "Motor ligado". Em seguida, crie uma struct Carro que embute Motor e um campo Modelo (string). Instancie um Carro e chame o método Ligar().

    ✓ Resposta:
    package main
    
    import "fmt"
    
    type Motor struct {
        Potencia int
    }
    
    func (m Motor) Ligar() {
        fmt.Println("Motor ligado")
    }
    
    type Carro struct {
        Modelo string
        Motor
    }
    
    func main() {
        c := Carro{Modelo: "Fusca", Motor: Motor{Potencia: 100}}
        c.Ligar() // Motor ligado
    }
  2. Defina uma interface Imprimivel com método Imprimir() e outra interface Escaneavel com método Escaneer(). Crie uma interface ImpressoraMultifuncional que embute ambas. Implemente essas interfaces em uma struct Impressora.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    type Imprimivel interface {
        Imprimir()
    }
    
    type Escaneavel interface {
        Escanear()
    }
    
    type ImpressoraMultifuncional interface {
        Imprimivel
        Escaneavel
    }
    
    type Impressora struct{}
    
    func (i Impressora) Imprimir() {
        fmt.Println("Imprimindo...")
    }
    
    func (i Impressora) Escanear() {
        fmt.Println("Escaneando...")
    }
    
    func main() {
        var im ImpressoraMultifuncional = Impressora{}
        im.Imprimir()
        im.Escanear()
    }
  3. Crie uma struct Base com método Metodo() que retorna "Base". Crie uma struct Derivada que embute Base e sobrescreve Metodo() retornando "Derivada". Mostre como chamar o método da base explicitamente.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    type Base struct{}
    
    func (b Base) Metodo() string {
        return "Base"
    }
    
    type Derivada struct {
        Base
    }
    
    func (d Derivada) Metodo() string {
        return "Derivada"
    }
    
    func main() {
        d := Derivada{}
        fmt.Println(d.Metodo())      // Derivada
        fmt.Println(d.Base.Metodo()) // Base
    }
  4. Explique por que a composição é preferível à herança em Go, citando pelo menos duas vantagens.

    ✓ Resposta: A composição é preferível porque oferece maior flexibilidade (permite combinar comportamentos sem hierarquia rígida) e menor acoplamento (os componentes são independentes e reutilizáveis). Além disso, evita problemas como o diamante da herança múltipla.
  5. Crie uma struct Logger com método Log(mensagem string) que imprime a mensagem. Crie uma struct Servidor que embute Logger e tem um campo Nome. Instancie um servidor e chame o método Log diretamente.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    type Logger struct{}
    
    func (l Logger) Log(mensagem string) {
        fmt.Println(mensagem)
    }
    
    type Servidor struct {
        Nome string
        Logger
    }
    
    func main() {
        s := Servidor{Nome: "MeuServidor"}
        s.Log("Servidor iniciado") // Servidor iniciado
    }