Em Go, não existe herança clássica como em linguagens orientadas a objetos. Em vez disso, a linguagem promove a composição por meio de embedding (ou incorporação). O embedding permite que um tipo inclua outro tipo anonimamente, herdando seus campos e métodos. Isso é uma forma poderosa de reutilização de código, combinada com a simplicidade do modelo de tipos de Go.

Nesta aula, veremos como funciona o embedding de structs e interfaces, como os métodos são promovidos e como essa abordagem se compara à herança tradicional. Ao final, você entenderá por que a composição é preferida em Go e como usá-la efetivamente.

Embedding de structs

O embedding de structs ocorre quando declaramos um campo de struct sem nome, apenas com o tipo. Isso faz com que os campos e métodos do tipo embutido sejam promovidos para o struct externo, como se fossem declarados diretamente nele.

Por exemplo, suponha que temos uma struct Base com um campo ID e um método GetID. Ao incorporar Base em User, podemos acessar user.ID e chamar user.GetID() diretamente.

type Base struct {
    ID int
}

func (b Base) GetID() int {
    return b.ID
}

type User struct {
    Base
    Name string
}

func main() {
    u := User{Base: Base{ID: 1}, Name: "Alice"}
    fmt.Println(u.ID)      // 1
    fmt.Println(u.GetID()) // 1
}

Note que o campo embutido é acessado pelo nome do tipo (ex.: u.Base.ID), mas quando não há ambiguidade, podemos usar o nome do campo diretamente. Se houver conflito (ex.: ambos têm campo Name), o campo do struct externo prevalece, e o campo interno deve ser acessado explicitamente.

Embedding de interfaces

Também é possível embutir interfaces em outras interfaces. Isso é usado para compor interfaces maiores a partir de interfaces menores, sem redefinir métodos.

Por exemplo, podemos ter uma interface Reader e uma Writer, e combiná-las em ReadWriter embutindo ambas:

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
}

Qualquer tipo que implemente todos os métodos de Reader e Writer automaticamente implementa ReadWriter. Isso é semelhante à herança múltipla de interfaces, mas sem complexidades de herança de classe.

Além disso, é possível embutir interfaces em structs. Isso é útil para criar structs que implementam interfaces parciais ou para injetar comportamentos:

type Logger interface {
    Log(message string)
}

type ConsoleLogger struct{}

func (c ConsoleLogger) Log(msg string) {
    fmt.Println(msg)
}

type Server struct {
    Logger // interface embutida
}

func main() {
    s := Server{Logger: ConsoleLogger{}}
    s.Log("servidor iniciado") // delega para ConsoleLogger.Log
}

Promoção de métodos

A promoção de métodos é o mecanismo pelo qual métodos de tipos embutidos se tornam acessíveis no tipo externo. Isso vale tanto para structs quanto para interfaces. Quando um método é chamado em um valor do tipo externo, Go procura primeiro nos métodos declarados diretamente no tipo. Se não encontrar, procura nos métodos dos tipos embutidos, recursivamente.

Se houver métodos com o mesmo nome em diferentes níveis de embedding, o método do tipo mais externo tem prioridade (shadowing). Para chamar o método do tipo embutido, é necessário acessá-lo explicitamente.

type A struct{}
func (a A) Foo() { fmt.Println("A.Foo") }

type B struct {
    A
}
func (b B) Foo() { fmt.Println("B.Foo") }

type C struct {
    B
}

type D struct {
    C
}

func main() {
    d := D{}
    d.Foo()       // B.Foo (prioridade de B sobre A)
    d.B.Foo()     // B.Foo
    d.B.A.Foo()   // A.Foo
}

A promoção de métodos é uma característica fundamental que torna o embedding útil: permite que tipos compostos herdem comportamentos sem complexidade adicional.

vs herança

Diferentemente da herança em linguagens como Java ou C++, o embedding em Go não cria uma relação de subtipo. Um struct que embute outro não é considerado um subtipo do tipo embutido. Ou seja, User não é um Base. No entanto, se User embute Base, ele herda os métodos de Base, mas não pode ser usado onde um Base é esperado, a menos que implemente uma interface comum.

Na herança tradicional, uma subclasse herda todo o comportamento da superclasse e pode sobrescrever métodos. Em Go, a composição via embedding é mais flexível: você pode escolher quais comportamentos incluir, sem a rigidez de uma hierarquia fixa. Além disso, Go não permite herança múltipla de classes (apenas de interfaces), evitando problemas como o "diamante".

Em resumo, o embedding em Go promove a reutilização por composição, que é geralmente preferida em relação à herança por ser mais modular e fácil de manter. A máxima "prefira composição à herança" é levada a sério em Go.

Boas práticas

  • Use embedding para estender funcionalidades de forma limpa, mas evite criar hierarquias profundas (mais de dois níveis).
  • Prefira interfaces pequenas e específicas, combinando-as via embedding quando necessário.
  • Cuidado com conflitos de nomes: se o struct externo e o embutido têm campos/métodos com o mesmo nome, o externo prevalece. Acesse o interno explicitamente se precisar.
  • Lembre-se de que embedding não cria subtipos. Se você precisa de polimorfismo, use interfaces.

Referências

Exercícios

  1. Crie uma struct Endereco com campos Rua e Numero. Em seguida, crie uma struct Pessoa que embute Endereco e tem um campo Nome. Instancie uma pessoa e acesse seus campos.

    ✓ Resposta:
    type Endereco struct {
        Rua    string
        Numero int
    }
    
    type Pessoa struct {
        Endereco
        Nome string
    }
    
    func main() {
        p := Pessoa{Endereco: Endereco{Rua: "Av. Paulista", Numero: 1000}, Nome: "João"}
        fmt.Println(p.Nome)    // João
        fmt.Println(p.Rua)     // Av. Paulista (promovido)
        fmt.Println(p.Numero)  // 1000
    }
  2. Defina uma interface Imprimivel com método Imprimir() string. Crie uma struct Documento que implemente essa interface. Depois, crie uma struct Relatorio que embute Documento e sobrescreva o método Imprimir. Demonstre a chamada do método original.

    ✓ Resposta:
    type Imprimivel interface {
        Imprimir() string
    }
    
    type Documento struct {
        Conteudo string
    }
    
    func (d Documento) Imprimir() string {
        return d.Conteudo
    }
    
    type Relatorio struct {
        Documento
        Autor string
    }
    
    func (r Relatorio) Imprimir() string {
        return r.Autor + ": " + r.Documento.Imprimir()
    }
    
    func main() {
        doc := Documento{Conteudo: "Conteúdo do documento"}
        rel := Relatorio{Documento: doc, Autor: "Maria"}
        fmt.Println(rel.Imprimir())            // Maria: Conteúdo do documento
        fmt.Println(rel.Documento.Imprimir())   // Conteúdo do documento
    }
  3. Explique por que o código a seguir não compila e como corrigi-lo usando embedding:

    type Animal struct {
        Nome string
    }
    
    func (a Animal) Falar() string {
        return "..."
    }
    
    type Cachorro struct {
        Animal
    }
    
    func (c Cachorro) Falar() string {
        return "Au au"
    }
    
    func main() {
        var a Animal = Cachorro{} // erro?
    }

    ✓ Resposta:O código não compila porque Cachorro não é um Animal (não há herança de tipo). Para corrigir, use uma interface. Crie uma interface SerVivo que exija o método Falar e então atribua Cachorro a uma variável do tipo SerVivo:
  4. Crie uma interface Leitor com método Ler() string e uma interface Escritor com método Escrever(s string). Em seguida, crie uma interface LeitorEscritor que embuta ambas. Implemente as interfaces em uma struct Arquivo e use-a.

    ✓ Resposta:
    type Leitor interface {
        Ler() string
    }
    
    type Escritor interface {
        Escrever(s string)
    }
    
    type LeitorEscritor interface {
        Leitor
        Escritor
    }
    
    type Arquivo struct {
        conteudo string
    }
    
    func (a *Arquivo) Ler() string {
        return a.conteudo
    }
    
    func (a *Arquivo) Escrever(s string) {
        a.conteudo = s
    }
    
    func main() {
        var le LeitorEscritor = &Arquivo{}
        le.Escrever("Olá, mundo!")
        fmt.Println(le.Ler()) // Olá, mundo!
    }
  5. Considere as structs Base e Derivada:

    type Base struct {
        Valor int
    }
    
    func (b Base) Get() int { return b.Valor }
    
    type Derivada struct {
        Base
        Valor int
    }
    

    O que será impresso por d.Get() e d.Base.Get() se d := Derivada{Base: Base{Valor: 10}, Valor: 20}?

    ✓ Resposta:d.Get() retorna 10 (método promovido de Base, que usa o campo Valor de Base). d.Base.Get() também retorna 10. O campo Valor de Derivada não é acessado pelo método Get de Base. Para acessar o campo de Derivada, seria necessário um método próprio.