Go

O que Todo Dev Deve Saber sobre Embedding em Go: Composição de Structs e Interfaces

9 min de leitura

O que Todo Dev Deve Saber sobre Embedding em Go: Composição de Structs e Interfaces

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

<h2>Embedding em Go: O que é e Por Que Importa</h2>

<p>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ê &quot;embutir&quot; um tipo dentro de outro para aproveitar seus campos e métodos.</p>

<p>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.</p>

<h2>Embedding de Structs: Composição Prática</h2>

<h3>Conceito Fundamental</h3>

<p>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.</p>

<p>Considere um exemplo prático: você tem uma struct <code>Animal</code> com propriedades comuns e quer criar um <code>Cachorro</code> que herda essas características. Em Go, você não herda, você compõe:</p>

<pre><code class="language-go">package main

import &quot;fmt&quot;

type Animal struct {

Nome string

Idade int

}

func (a Animal) Fazer() string {

return fmt.Sprintf(&quot;%s tem %d anos&quot;, a.Nome, a.Idade)

}

type Cachorro struct {

Animal // Embedding sem nome de campo

Raca string

}

func main() {

dog := Cachorro{

Animal: Animal{Nome: &quot;Rex&quot;, Idade: 5},

Raca: &quot;Labrador&quot;,

}

// Acesso direto aos campos promovidos

fmt.Println(dog.Nome) // Rex

fmt.Println(dog.Fazer()) // Rex tem 5 anos

fmt.Println(dog.Raca) // Labrador

}</code></pre>

<p>Observe que <code>Cachorro</code> não precisa definir seu próprio campo <code>Nome</code> — ele herda esse acesso automaticamente. O método <code>Fazer()</code> também é promovido e pode ser chamado diretamente em instâncias de <code>Cachorro</code>. Esta é a beleza do embedding: simplicidade com reutilização.</p>

<h3>Shadowing: Quando Você Sobrescreve Comportamento</h3>

<p>À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 &quot;sombra&quot; o método interno, mas você ainda pode acessar o método original através do nome do tipo embutido.</p>

<pre><code class="language-go">package main

import &quot;fmt&quot;

type Veiculo struct {

Marca string

Ano int

}

func (v Veiculo) Descricao() string {

return fmt.Sprintf(&quot;Veículo %s (%d)&quot;, v.Marca, v.Ano)

}

type Carro struct {

Veiculo

Portas int

}

// Shadowing: redefinindo o método Descricao

func (c Carro) Descricao() string {

return fmt.Sprintf(&quot;%s com %d portas&quot;, c.Veiculo.Descricao(), c.Portas)

}

func main() {

car := Carro{

Veiculo: Veiculo{Marca: &quot;Toyota&quot;, Ano: 2022},

Portas: 4,

}

fmt.Println(car.Descricao()) // Veículo Toyota (2022) com 4 portas

fmt.Println(car.Veiculo.Descricao()) // Veículo Toyota (2022)

}</code></pre>

<p>Aqui, <code>Carro</code> redefine <code>Descricao()</code>, mas ainda pode chamar o método original através de <code>c.Veiculo.Descricao()</code>. Isso oferece flexibilidade: você estende funcionalidade sem perder o acesso à implementação original.</p>

<h2>Embedding de Interfaces: Composição de Comportamentos</h2>

<h3>Combinando Interfaces</h3>

<p>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.</p>

<pre><code class="language-go">package main

import &quot;fmt&quot;

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(&quot;Escrito: %s\n&quot;, data)

return nil

}

func Processar(rw LeitorEscritor) {

dados := rw.Ler()

rw.Escrever(fmt.Sprintf(&quot;Processado: %s&quot;, dados))

}

func main() {

arquivo := &amp;Arquivo{conteudo: &quot;dados originais&quot;}

Processar(arquivo)

fmt.Println(arquivo.Ler()) // Processado: dados originais

}</code></pre>

<p>Neste exemplo, <code>LeitorEscritor</code> embutir <code>Leitor</code> e <code>Escritor</code>, criando uma interface que exige ambos os comportamentos. Qualquer tipo que implemente ambas as interfaces satisfaz <code>LeitorEscritor</code>. Isso reduz duplicação e torna seu código mais composável.</p>

<h3>Interfaces Vazias e Flexibilidade</h3>

<p>A interface vazia <code>interface{}</code> em Go é especial: todo tipo a implementa automaticamente. Quando você embutir <code>interface{}</code> 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.</p>

<pre><code class="language-go">package main

import (

&quot;fmt&quot;

&quot;reflect&quot;

)

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: &quot;texto&quot;}

fmt.Println(caixa2.Tipo()) // string

}</code></pre>

<h2>Padrões Avançados: Casos de Uso Reais</h2>

<h3>Logging e Middleware com Embedding</h3>

<p>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.</p>

<pre><code class="language-go">package main

import (

&quot;fmt&quot;

&quot;log&quot;

)

type Servico interface {

Processar(id int) string

}

type ServicoReal struct{}

func (s ServicoReal) Processar(id int) string {

return fmt.Sprintf(&quot;Processando ID: %d&quot;, id)

}

type ServicoComLog struct {

Servico // Embutindo a interface

}

func (s ServicoComLog) Processar(id int) string {

log.Printf(&quot;Iniciando processamento com ID: %d\n&quot;, id)

resultado := s.Servico.Processar(id)

log.Printf(&quot;Resultado: %s\n&quot;, 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)

}</code></pre>

<p>Aqui, <code>ServicoComLog</code> embutir a interface <code>Servico</code>, permitindo que você decore qualquer implementação com logging. O padrão Decorator é implementado naturalmente através do embedding.</p>

<h3>Extensão de Tipos Terceirizados</h3>

<p>À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.</p>

<pre><code class="language-go">package main

import (

&quot;fmt&quot;

&quot;time&quot;

)

type Relogio struct {

time.Time

}

func (r Relogio) Formatado() string {

return r.Format(&quot;02/01/2006 15:04:05&quot;)

}

func main() {

agora := Relogio{Time: time.Now()}

fmt.Println(agora.Formatado())

fmt.Println(agora.Year()) // Método promovido de time.Time

}</code></pre>

<p>Este padrão é extremamente útil quando você precisa adicionar métodos a tipos que não controla.</p>

<h2>Conclusão</h2>

<p>Embedding é um dos pilares da filosofia de design de Go e oferece uma alternativa elegante à herança tradicional. Primeiro, <strong>structs embutidas promovem campos e métodos automaticamente</strong>, permitindo composição clara sem a complexidade de hierarquias de classes. Segundo, <strong>interfaces embutidas permitem composição de comportamentos</strong>, facilitando a criação de abstrações modulares e testáveis. Terceiro, <strong>o padrão é flexível o suficiente para decoradores, middleware e extensão de tipos</strong>, resolvendo problemas práticos com simplicidade.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://golang.org/doc/effective_go#embedding" target="_blank" rel="noopener noreferrer">Effective Go - Embedding</a></li>

<li><a href="https://golang.org/ref/spec#Struct_types" target="_blank" rel="noopener noreferrer">The Go Programming Language - Structs</a></li>

<li><a href="https://research.swtch.com/interfaces" target="_blank" rel="noopener noreferrer">Go Interfaces - Russ Cox</a></li>

<li><a href="https://dave.cheney.net/2015/10/31/compiler-optimisation-tips-for-c-developers" target="_blank" rel="noopener noreferrer">Composing Simple Types in Go - Dave Cheney</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

O que Todo Dev Deve Saber sobre Containerizando Aplicações Go: Dockerfile Multi-stage e Distroless
O que Todo Dev Deve Saber sobre Containerizando Aplicações Go: Dockerfile Multi-stage e Distroless

Entendendo o Problema: Por Que Multi-stage e Distroless? Quando você conteine...

Dominando Domain-Driven Design em Go: Entidades, Value Objects e Repositórios em Projetos Reais
Dominando Domain-Driven Design em Go: Entidades, Value Objects e Repositórios em Projetos Reais

Domain-Driven Design: Fundamentos e Aplicação em Go Domain-Driven Design (DDD...

O que Todo Dev Deve Saber sobre Type Switch em Go: Discriminando Tipos em Tempo de Execução
O que Todo Dev Deve Saber sobre Type Switch em Go: Discriminando Tipos em Tempo de Execução

O que é Type Switch e Por que Usar Type switch é um mecanismo em Go que permi...