Embedding em Go: O que é e Por Que Importa
Embedding é um mecanismo poderoso em Go que permite que você reutilize código e comportamento através da composição, não herança. Diferentemente de linguagens como Java ou C++, Go não oferece herança tradicional com classes. Em vez disso, usa composição: você "embutir" um tipo dentro de outro para aproveitar seus campos e métodos.
Quando você embutir uma struct ou interface dentro de outra, o Go automaticamente promove os campos e métodos do tipo embutido para o tipo externo. Isso significa que você pode acessar membros diretos sem precisar especificar o caminho completo. É uma forma elegante de reutilizar funcionalidades mantendo código simples e testável.
Embedding de Structs: Composição Prática
Conceito Fundamental
O embedding de structs é a forma mais comum e intuitiva de reutilizar código em Go. Quando você inclui um tipo nomeado (outra struct) sem um nome de campo dentro de uma struct, os campos e métodos daquele tipo são promovidos automaticamente. Você acessa-os diretamente como se fossem do tipo externo.
Considere um exemplo prático: você tem uma struct Animal com propriedades comuns e quer criar um Cachorro que herda essas características. Em Go, você não herda, você compõe:
package main
import "fmt"
type Animal struct {
Nome string
Idade int
}
func (a Animal) Fazer() string {
return fmt.Sprintf("%s tem %d anos", a.Nome, a.Idade)
}
type Cachorro struct {
Animal // Embedding sem nome de campo
Raca string
}
func main() {
dog := Cachorro{
Animal: Animal{Nome: "Rex", Idade: 5},
Raca: "Labrador",
}
// Acesso direto aos campos promovidos
fmt.Println(dog.Nome) // Rex
fmt.Println(dog.Fazer()) // Rex tem 5 anos
fmt.Println(dog.Raca) // Labrador
}
Observe que Cachorro não precisa definir seu próprio campo Nome — ele herda esse acesso automaticamente. O método Fazer() também é promovido e pode ser chamado diretamente em instâncias de Cachorro. Esta é a beleza do embedding: simplicidade com reutilização.
Shadowing: Quando Você Sobrescreve Comportamento
Às vezes, você precisa modificar ou estender o comportamento de um tipo embutido. Em Go, isso é feito redefinindo um método no tipo externo. Quando você faz isso, o método externo "sombra" o método interno, mas você ainda pode acessar o método original através do nome do tipo embutido.
package main
import "fmt"
type Veiculo struct {
Marca string
Ano int
}
func (v Veiculo) Descricao() string {
return fmt.Sprintf("Veículo %s (%d)", v.Marca, v.Ano)
}
type Carro struct {
Veiculo
Portas int
}
// Shadowing: redefinindo o método Descricao
func (c Carro) Descricao() string {
return fmt.Sprintf("%s com %d portas", c.Veiculo.Descricao(), c.Portas)
}
func main() {
car := Carro{
Veiculo: Veiculo{Marca: "Toyota", Ano: 2022},
Portas: 4,
}
fmt.Println(car.Descricao()) // Veículo Toyota (2022) com 4 portas
fmt.Println(car.Veiculo.Descricao()) // Veículo Toyota (2022)
}
Aqui, Carro redefine Descricao(), mas ainda pode chamar o método original através de c.Veiculo.Descricao(). Isso oferece flexibilidade: você estende funcionalidade sem perder o acesso à implementação original.
Embedding de Interfaces: Composição de Comportamentos
Combinando Interfaces
Assim como você pode embutir structs, também pode embutir interfaces dentro de outras interfaces. Isso permite que você crie interfaces maiores e mais especializadas a partir de interfaces menores e focadas. Este é um padrão excelente para manter código modular e testável.
package main
import "fmt"
type Leitor interface {
Ler() string
}
type Escritor interface {
Escrever(data string) error
}
// Interface composta (embedding)
type LeitorEscritor interface {
Leitor
Escritor
}
type Arquivo struct {
conteudo string
}
func (a *Arquivo) Ler() string {
return a.conteudo
}
func (a *Arquivo) Escrever(data string) error {
a.conteudo = data
fmt.Printf("Escrito: %s\n", data)
return nil
}
func Processar(rw LeitorEscritor) {
dados := rw.Ler()
rw.Escrever(fmt.Sprintf("Processado: %s", dados))
}
func main() {
arquivo := &Arquivo{conteudo: "dados originais"}
Processar(arquivo)
fmt.Println(arquivo.Ler()) // Processado: dados originais
}
Neste exemplo, LeitorEscritor embutir Leitor e Escritor, criando uma interface que exige ambos os comportamentos. Qualquer tipo que implemente ambas as interfaces satisfaz LeitorEscritor. Isso reduz duplicação e torna seu código mais composável.
Interfaces Vazias e Flexibilidade
A interface vazia interface{} em Go é especial: todo tipo a implementa automaticamente. Quando você embutir interface{} em uma interface customizada, você cria um tipo que pode lidar com qualquer coisa. Isso deve ser usado com cuidado, pois reduz segurança de tipos, mas é poderoso em situações onde você precisa de máxima flexibilidade.
package main
import (
"fmt"
"reflect"
)
type Contenedor interface {
interface{}
Tipo() string
}
type Caixa struct {
valor interface{}
}
func (c Caixa) Tipo() string {
return reflect.TypeOf(c.valor).String()
}
func main() {
caixa := Caixa{valor: 42}
fmt.Println(caixa.Tipo()) // int
caixa2 := Caixa{valor: "texto"}
fmt.Println(caixa2.Tipo()) // string
}
Padrões Avançados: Casos de Uso Reais
Logging e Middleware com Embedding
Um padrão comum é usar embedding para adicionar funcionalidades transversais como logging, validação ou autorização. Você embutir uma interface que representa a dependência e promove seus métodos no tipo externo.
package main
import (
"fmt"
"log"
)
type Servico interface {
Processar(id int) string
}
type ServicoReal struct{}
func (s ServicoReal) Processar(id int) string {
return fmt.Sprintf("Processando ID: %d", id)
}
type ServicoComLog struct {
Servico // Embutindo a interface
}
func (s ServicoComLog) Processar(id int) string {
log.Printf("Iniciando processamento com ID: %d\n", id)
resultado := s.Servico.Processar(id)
log.Printf("Resultado: %s\n", resultado)
return resultado
}
func Executar(srv Servico, id int) {
fmt.Println(srv.Processar(id))
}
func main() {
srvReal := ServicoReal{}
srvComLog := ServicoComLog{Servico: srvReal}
Executar(srvComLog, 123)
}
Aqui, ServicoComLog embutir a interface Servico, permitindo que você decore qualquer implementação com logging. O padrão Decorator é implementado naturalmente através do embedding.
Extensão de Tipos Terceirizados
Às vezes, você precisa estender o comportamento de um tipo que não pode ser modificado (porque vem de uma biblioteca externa). Você pode embutir esse tipo em uma nova struct e adicionar métodos.
package main
import (
"fmt"
"time"
)
type Relogio struct {
time.Time
}
func (r Relogio) Formatado() string {
return r.Format("02/01/2006 15:04:05")
}
func main() {
agora := Relogio{Time: time.Now()}
fmt.Println(agora.Formatado())
fmt.Println(agora.Year()) // Método promovido de time.Time
}
Este padrão é extremamente útil quando você precisa adicionar métodos a tipos que não controla.
Conclusão
Embedding é um dos pilares da filosofia de design de Go e oferece uma alternativa elegante à herança tradicional. Primeiro, structs embutidas promovem campos e métodos automaticamente, permitindo composição clara sem a complexidade de hierarquias de classes. Segundo, interfaces embutidas permitem composição de comportamentos, facilitando a criação de abstrações modulares e testáveis. Terceiro, o padrão é flexível o suficiente para decoradores, middleware e extensão de tipos, resolvendo problemas práticos com simplicidade.