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 tipoRígida — hierarquia fixa
Sem acoplamento forteAcoplamento forte entre pai e filho
Fácil de refatorarDifícil mudar hierarquias existentes
Promove interfaces pequenasTende 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

  1. Prefira interfaces pequenas — combine-as via embedding em vez de criar interfaces grandes
  2. Embedding não é herança — não use para modelar hierarquias “é um”
  3. Cuidado com a API promovida — todos os métodos do tipo embarcado ficam visíveis
  4. Use embedding para reutilização — não para polimorfismo (use interfaces para isso)
  5. 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.