O que é Composition em Go?
Composition (composição) em Go é o principal mecanismo de reutilização de código e construção de tipos complexos. Diferente de linguagens como Java, C++ ou Python, Go não tem herança. Em vez disso, Go usa composição — combinar tipos menores para criar tipos maiores e mais complexos.
A ideia central é simples: em vez de dizer “um tipo é outro tipo” (herança), em Go dizemos “um tipo tem outro tipo” (composição). Esse design é intencional e segue o princípio clássico de engenharia de software: “favoreça composição sobre herança”.
Em Go, a composição é implementada principalmente através de struct embedding e interface embedding, que permitem reutilizar campos e métodos sem hierarquias de classes.
Struct embedding
O embedding de structs é a forma mais comum de composição em Go. Quando você inclui um tipo como campo anônimo dentro de outra struct, seus campos e métodos são promovidos — ficam acessíveis diretamente:
type Endereco struct {
Rua string
Cidade string
Estado string
CEP string
}
func (e Endereco) Formatado() string {
return fmt.Sprintf("%s, %s - %s, %s", e.Rua, e.Cidade, e.Estado, e.CEP)
}
type Pessoa struct {
Nome string
Email string
Endereco // embedding — campo anônimo
}
func main() {
p := Pessoa{
Nome: "Maria Silva",
Email: "maria@email.com",
Endereco: Endereco{
Rua: "Rua das Flores, 123",
Cidade: "São Paulo",
Estado: "SP",
CEP: "01234-567",
},
}
// Campos promovidos — acesso direto
fmt.Println(p.Cidade) // "São Paulo"
fmt.Println(p.CEP) // "01234-567"
// Método promovido — chamada direta
fmt.Println(p.Formatado()) // "Rua das Flores, 123, São Paulo - SP, 01234-567"
// Acesso explícito ao tipo embedded também funciona
fmt.Println(p.Endereco.Cidade) // "São Paulo"
}
Os campos e métodos do tipo embarcado ficam disponíveis como se fossem do tipo externo. Isso é composição, não herança — Pessoa não é um Endereco, mas tem um Endereco.
Composição múltipla
Você pode embarcar múltiplos tipos em uma struct, combinando comportamentos:
type Logger struct {
Prefixo string
}
func (l Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.Prefixo, msg)
}
type Metricas struct {
Contagem int
}
func (m *Metricas) Incrementar() {
m.Contagem++
}
func (m Metricas) Total() int {
return m.Contagem
}
type Servico struct {
Nome string
Logger
*Metricas
}
func main() {
svc := &Servico{
Nome: "API",
Logger: Logger{Prefixo: "api-svc"},
Metricas: &Metricas{},
}
svc.Log("Iniciando serviço") // método do Logger
svc.Incrementar() // método de Metricas
svc.Incrementar()
svc.Log(fmt.Sprintf("Requisições: %d", svc.Total()))
}
Esse padrão é usado extensivamente em projetos Go reais para adicionar funcionalidades como logging, métricas e contexto a serviços.
Disambiguation (desambiguação)
Quando dois tipos embarcados têm campos ou métodos com o mesmo nome, Go exige que você especifique qual usar:
type Brasileiro struct {
Nome string
}
type Profissional struct {
Nome string
}
type Funcionario struct {
Brasileiro
Profissional
}
func main() {
f := Funcionario{
Brasileiro: Brasileiro{Nome: "Carlos"},
Profissional: Profissional{Nome: "Dr. Carlos"},
}
// fmt.Println(f.Nome) // ERRO: ambíguo — qual Nome?
// Solução: acesso explícito
fmt.Println(f.Brasileiro.Nome) // "Carlos"
fmt.Println(f.Profissional.Nome) // "Dr. Carlos"
}
Se o tipo externo define seu próprio campo ou método com o mesmo nome, ele tem prioridade sobre os promovidos:
type Base struct {
ID int
}
func (b Base) Tipo() string {
return "base"
}
type Derivado struct {
Base
}
// Override — este método tem prioridade sobre Base.Tipo()
func (d Derivado) Tipo() string {
return "derivado"
}
func main() {
d := Derivado{Base: Base{ID: 1}}
fmt.Println(d.Tipo()) // "derivado"
fmt.Println(d.Base.Tipo()) // "base" — acesso direto ao original
}
Interface embedding
Composição também funciona com interfaces. Você pode combinar interfaces menores em interfaces maiores:
type Leitor interface {
Ler(p []byte) (int, error)
}
type Escritor interface {
Escrever(p []byte) (int, error)
}
type Fechador interface {
Fechar() error
}
// Composição de interfaces
type LeitorEscritor interface {
Leitor
Escritor
}
type LeitorEscritorFechador interface {
Leitor
Escritor
Fechador
}
A biblioteca padrão de Go usa esse padrão extensivamente. O pacote io define interfaces compostas como:
// Da biblioteca padrão
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Esse padrão de interfaces pequenas e compostas é considerado idiomático em Go. Veja mais na entrada sobre interface.
Embedding para satisfazer interfaces
Um dos usos mais poderosos de embedding é satisfazer interfaces automaticamente através de métodos promovidos:
type Animal interface {
Nome() string
Som() string
}
type InfoBase struct {
NomeAnimal string
}
func (i InfoBase) Nome() string {
return i.NomeAnimal
}
type Cachorro struct {
InfoBase
Raca string
}
// Cachorro só precisa implementar Som() —
// Nome() já vem promovido de InfoBase
func (c Cachorro) Som() string {
return "Au au!"
}
func main() {
var a Animal = Cachorro{
InfoBase: InfoBase{NomeAnimal: "Rex"},
Raca: "Pastor Alemão",
}
fmt.Printf("%s faz %s\n", a.Nome(), a.Som())
// Rex faz Au au!
}
Isso permite construir tipos que satisfazem interfaces complexas reutilizando implementações parciais — um padrão muito útil em testes e na criação de mocks.
Padrões reais de composição
Middleware HTTP
Composição é amplamente usada para criar middleware em servidores HTTP:
type LogMiddleware struct {
Handler http.Handler
Logger *log.Logger
}
func (lm *LogMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
lm.Logger.Printf("%s %s", r.Method, r.URL.Path)
lm.Handler.ServeHTTP(w, r)
}
type AuthMiddleware struct {
Handler http.Handler
Token string
}
func (am *AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != am.Token {
http.Error(w, "não autorizado", http.StatusUnauthorized)
return
}
am.Handler.ServeHTTP(w, r)
}
Cada middleware envolve um http.Handler e adiciona comportamento. Essa cadeia de composição é fundamental no ecossistema web de Go, como descrito no tutorial de API REST com Go.
Repository pattern
type Repositorio struct {
db *sql.DB
}
type UsuarioRepo struct {
Repositorio // herda conexão do DB
}
func (ur *UsuarioRepo) BuscarPorID(id int) (*Usuario, error) {
row := ur.db.QueryRow("SELECT nome, email FROM usuarios WHERE id = $1", id)
u := &Usuario{}
err := row.Scan(&u.Nome, &u.Email)
return u, err
}
type ProdutoRepo struct {
Repositorio // mesmo padrão
}
func (pr *ProdutoRepo) Listar() ([]Produto, error) {
// usa pr.db herdado de Repositorio
rows, err := pr.db.Query("SELECT nome, preco FROM produtos")
// ...
return nil, err
}
Esse padrão é comum em projetos que usam PostgreSQL e clean architecture.
Composição vs herança
| Composição (Go) | Herança (Java/C++) |
|---|---|
| “tem um” (has-a) | “é um” (is-a) |
| Flexível — combina qualquer tipo | Rígida — hierarquia fixa |
| Sem acoplamento forte | Acoplamento forte entre pai e filho |
| Fácil de refatorar | Difícil mudar hierarquias existentes |
| Promove interfaces pequenas | Tende a interfaces grandes |
| Sem “diamond problem” | Herança múltipla causa conflitos |
Para entender como Go compensa a falta de herança, veja também as entradas sobre interface e generic.
Boas práticas
- Prefira interfaces pequenas — combine-as via embedding em vez de criar interfaces grandes
- Embedding não é herança — não use para modelar hierarquias “é um”
- Cuidado com a API promovida — todos os métodos do tipo embarcado ficam visíveis
- Use embedding para reutilização — não para polimorfismo (use interfaces para isso)
- Teste independentemente — tipos embarcados devem ser testáveis por conta própria
Para mais sobre organização de código e reutilização em Go, consulte o tutorial de microserviços com Go e a entrada sobre module.
Perguntas frequentes (FAQ)
Go tem herança?
Não. Go deliberadamente não implementa herança. Em vez disso, usa composição via struct embedding e interface embedding. Esse design evita problemas comuns de herança como o “diamond problem”, acoplamento forte e hierarquias rígidas. A filosofia de Go segue o princípio “favoreça composição sobre herança” do livro Design Patterns.
Qual a diferença entre embedding e campo nomeado?
Quando você usa embedding (campo anônimo), os campos e métodos do tipo embarcado são promovidos — ficam acessíveis diretamente no tipo externo. Com um campo nomeado, você precisa acessar através do nome do campo. Embedding é ideal para reutilização de comportamento, enquanto campos nomeados são para relações de dados simples.
Posso embarcar múltiplos tipos em uma struct?
Sim. Go permite embarcar quantos tipos você precisar. Se dois tipos embarcados têm campos ou métodos com o mesmo nome, Go exige desambiguação — você deve acessar explicitamente através do nome do tipo embarcado. O tipo externo também pode definir seu próprio método com o mesmo nome, que terá prioridade sobre os promovidos.
Como composição se relaciona com interfaces?
Composição e interfaces trabalham juntas em Go. Interface embedding combina interfaces pequenas em maiores (como io.ReadWriter). Struct embedding pode satisfazer interfaces automaticamente via métodos promovidos. Juntas, permitem polimorfismo e reutilização de código sem herança — o padrão idiomático em Go.