Embedding (composição)
Nesta aula, exploramos o conceito de embedding (composição) em Go, que permite reutilizar campos e métodos de structs e interfaces sem herança. Aprendemos como embedding de structs e interfaces funciona, como a promoção de métodos ocorre e como essa abordagem difere da herança tradicional de linguagens orientadas a objetos.
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
- Effective Go: Embedding
- Go Specification: Struct types
- Go Specification: Interface types
- DigitalOcean: How To Use Struct Embedding in Go
- Medium: Struct Embedding in Go
Exercícios
Crie uma struct
Enderecocom camposRuaeNumero. Em seguida, crie uma structPessoaque embuteEnderecoe tem um campoNome. 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 }Defina uma interface
Imprimivelcom métodoImprimir() string. Crie uma structDocumentoque implemente essa interface. Depois, crie uma structRelatorioque embuteDocumentoe sobrescreva o métodoImprimir. 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 }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 porqueCachorronão é umAnimal(não há herança de tipo). Para corrigir, use uma interface. Crie uma interfaceSerVivoque exija o métodoFalare então atribuaCachorroa uma variável do tipoSerVivo:Crie uma interface
Leitorcom métodoLer() stringe uma interfaceEscritorcom métodoEscrever(s string). Em seguida, crie uma interfaceLeitorEscritorque embuta ambas. Implemente as interfaces em uma structArquivoe 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! }Considere as structs
BaseeDerivada: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()ed.Base.Get()sed := 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 campoValorde Derivada não é acessado pelo métodoGetde Base. Para acessar o campo de Derivada, seria necessário um método próprio.