Embedding (composição)
Esta aula aborda embedding (composição) em Go, um mecanismo que permite reutilizar campos e métodos de tipos aninhados. Serão explorados embedding de structs, embedding de interfaces, promoção de métodos e a diferença entre composição e herança.
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
- Effective Go - Embedding
- Go Specification - Struct types
- Go Specification - Interface types
- Go Advent Calendar - Embedding (Mat Ryer)
- DigitalOcean - How To Use Struct Embedding in Go
Exercícios
Crie uma struct
Motorcom campoPotencia(int) e métodoLigar()que imprime "Motor ligado". Em seguida, crie uma structCarroque embuteMotore um campoModelo(string). Instancie umCarroe chame o métodoLigar().✓ 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 }Defina uma interface
Imprimivelcom métodoImprimir()e outra interfaceEscaneavelcom métodoEscaneer(). Crie uma interfaceImpressoraMultifuncionalque embute ambas. Implemente essas interfaces em uma structImpressora.✓ 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() }Crie uma struct
Basecom métodoMetodo()que retorna "Base". Crie uma structDerivadaque embuteBasee sobrescreveMetodo()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 }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.Crie uma struct
Loggercom métodoLog(mensagem string)que imprime a mensagem. Crie uma structServidorque embuteLoggere tem um campoNome. Instancie um servidor e chame o métodoLogdiretamente.✓ 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 }