A herança é um dos pilares da orientação a objetos clássica. Em Java, C++ e C#, reutilizar comportamento significa criar hierarquias de classes — uma classe filha herda campos e métodos da classe pai, que pode herdar de outra, formando árvores de herança que crescem com o projeto.
Go rejeitou esse modelo deliberadamente. Não existe herança em Go. Não existe extends, não existe classe base, não existe polimorfismo por hierarquia de tipos. Em seu lugar, Go oferece dois mecanismos complementares: composição via embedding e polimorfismo via interfaces. A combinação dos dois cobre todos os casos de uso da herança — sem suas desvantagens.
O problema da herança profunda
Antes de ver como Go resolve o problema, vale entender o que Go está evitando. Hierarquias de herança profunda criam acoplamento rígido entre classes. Uma mudança na classe base pode quebrar silenciosamente todas as subclasses. O problema do diamante — quando uma classe herda de duas classes que compartilham um ancestral comum — exige regras de resolução complexas. E o pior: a herança força o design a ser decidido cedo, quando menos se sabe sobre o problema.
// Hierarquia típica em Java — frágil e rígida
Animal
└── Vertebrado
├── Mamífero
│ ├── Cão
│ └── Gato
└── Ave
├── Pinguim
└── Águia
O que acontece quando um Pinguim herda de Ave mas não pode voar? Quando um Ornitorrinco é mamífero mas bota ovos? A hierarquia quebra ou exige contorções.
Composição: construir com partes
O princípio da composição diz que um tipo deve ter comportamentos, não ser uma especialização de outro tipo. Em Go, isso é feito embutindo tipos dentro de outros tipos.
package main
import "fmt"
type Logger struct {
Prefixo string
}
func (l Logger) Log(mensagem string) {
fmt.Printf("[%s] %s\n", l.Prefixo, mensagem)
}
type ConexaoDB struct {
Host string
Porta int
}
func (c ConexaoDB) Conectar() {
fmt.Printf("Conectando em %s:%d\n", c.Host, c.Porta)
}
// Servico compõe Logger e ConexaoDB
type Servico struct {
Nome string
Logger
ConexaoDB
}
func main() {
s := Servico{
Nome: "PedidoService",
Logger: Logger{Prefixo: "INFO"},
ConexaoDB: ConexaoDB{Host: "localhost", Porta: 5432},
}
s.Log("Serviço iniciado") // método promovido de Logger
s.Conectar() // método promovido de ConexaoDB
fmt.Println(s.Nome)
}
Servico não é um Logger nem uma ConexaoDB. Ele tem um logger e tem uma conexão. Essa distinção semântica é fundamental — e reflete com mais precisão como os sistemas reais funcionam.
Composição com interfaces: o verdadeiro polimorfismo
A herança oferece polimorfismo por hierarquia — uma função que aceita Animal pode receber Cão ou Gato. Go oferece polimorfismo por interface — uma função que aceita Falante pode receber qualquer tipo que implemente Falar(), independentemente de onde esse tipo foi definido ou de quem o criou.
package main
import (
"fmt"
"math"
)
// Interfaces pequenas e focadas
type Descritivel interface {
Descricao() string
}
type Calculavel interface {
Calcular() float64
}
// Composição de interfaces
type Item interface {
Descritivel
Calculavel
}
type Produto struct {
Nome string
Preco float64
Qtd int
}
func (p Produto) Descricao() string {
return fmt.Sprintf("%s (x%d)", p.Nome, p.Qtd)
}
func (p Produto) Calcular() float64 {
return p.Preco * float64(p.Qtd)
}
type Desconto struct {
Descricao_ string
Percentual float64
Base float64
}
func (d Desconto) Descricao() string {
return fmt.Sprintf("%s (%.0f%%)", d.Descricao_, d.Percentual)
}
func (d Desconto) Calcular() float64 {
return -(d.Base * d.Percentual / 100)
}
func totalPedido(itens []Item) float64 {
total := 0.0
for _, item := range itens {
valor := item.Calcular()
fmt.Printf(" %-30s R$ %8.2f\n", item.Descricao(), valor)
total += valor
}
return total
}
func main() {
itens := []Item{
Produto{Nome: "Teclado Mecânico", Preco: 350.00, Qtd: 1},
Produto{Nome: "Mouse Gamer", Preco: 180.00, Qtd: 2},
Produto{Nome: "Mousepad XL", Preco: 90.00, Qtd: 1},
Desconto{Descricao_: "Cupom PROMO10", Percentual: 10, Base: 800.00},
}
fmt.Println("=== Resumo do Pedido ===")
total := totalPedido(itens)
fmt.Printf(" %-30s R$ %8.2f\n", "TOTAL", total)
}
Delegação explícita vs promoção
O embedding promove métodos automaticamente. Mas às vezes a delegação explícita — chamar o método do campo diretamente — é mais clara, especialmente quando o tipo externo precisa interceptar ou modificar o comportamento:
package main
import "fmt"
type Repositorio struct{}
func (r *Repositorio) Salvar(entidade string) error {
fmt.Println("Salvando:", entidade)
return nil
}
// RepositorioComLog intercepta o comportamento sem herança
type RepositorioComLog struct {
repo *Repositorio
logger Logger
}
func (r *RepositorioComLog) Salvar(entidade string) error {
r.logger.Log("Iniciando salvamento de: " + entidade)
err := r.repo.Salvar(entidade) // delegação explícita
if err != nil {
r.logger.Log("Erro ao salvar: " + err.Error())
return err
}
r.logger.Log("Salvamento concluído: " + entidade)
return nil
}
func main() {
repo := &RepositorioComLog{
repo: &Repositorio{},
logger: Logger{Prefixo: "REPO"},
}
repo.Salvar("Usuario{id:1, nome:Ana}")
}
Esse padrão é conhecido como Decorator — adiciona comportamento a um objeto sem modificar sua implementação original. Em Go, ele é implementado naturalmente através de composição, sem necessidade de nenhum framework.
Substituindo comportamento sem herança
Em linguagens com herança, sobrescrever um método significa redeclará-lo na subclasse. Em Go, o mecanismo equivalente é definir o método no tipo externo — ele terá precedência sobre o método promovido pelo embedding:
package main
import "fmt"
type Animal struct {
Nome string
}
func (a Animal) Falar() string {
return "..."
}
func (a Animal) Apresentar() {
fmt.Printf("%s diz: %s\n", a.Nome, a.Falar())
}
type Cachorro struct {
Animal
}
func (c Cachorro) Falar() string {
return "Au au!"
}
type Gato struct {
Animal
}
func (g Gato) Falar() string {
return "Miau!"
}
func main() {
c := Cachorro{Animal: Animal{Nome: "Rex"}}
g := Gato{Animal: Animal{Nome: "Whiskers"}}
fmt.Println(c.Falar()) // Au au!
fmt.Println(g.Falar()) // Miau!
// Atenção: Apresentar() chama Animal.Falar(), não Cachorro.Falar()
c.Apresentar() // Rex diz: ... ← não chama o método sobrescrito
g.Apresentar() // Whiskers diz: ...
}
Esse exemplo revela uma diferença importante entre embedding e herança real: métodos promovidos não são virtuais. Apresentar() pertence a Animal e chama Animal.Falar() — não o Falar() do tipo externo. Esse comportamento é previsível e explícito, mas surpreende quem espera polimorfismo por herança.
Para obter o comportamento polimórfico desejado, a solução é usar interfaces:
type Falante interface {
Falar() string
Apresentar()
}
// Cada tipo implementa Apresentar() diretamente
func (c Cachorro) Apresentar() {
fmt.Printf("%s diz: %s\n", c.Nome, c.Falar())
}
func (g Gato) Apresentar() {
fmt.Printf("%s diz: %s\n", g.Nome, g.Falar())
}
func apresentarTodos(animais []Falante) {
for _, a := range animais {
a.Apresentar()
}
}
func main() {
animais := []Falante{
Cachorro{Animal: Animal{Nome: "Rex"}},
Gato{Animal: Animal{Nome: "Whiskers"}},
}
apresentarTodos(animais)
// Rex diz: Au au!
// Whiskers diz: Miau!
}
O padrão funcional: injeção de comportamento
Outra forma de composição em Go é injetar comportamento através de funções ou interfaces como campos:
package main
import "fmt"
type Validador func(string) error
type FormularioCadastro struct {
validarEmail Validador
validarSenha Validador
}
func novoFormulario(validarEmail, validarSenha Validador) *FormularioCadastro {
return &FormularioCadastro{
validarEmail: validarEmail,
validarSenha: validarSenha,
}
}
func (f *FormularioCadastro) Submeter(email, senha string) error {
if err := f.validarEmail(email); err != nil {
return fmt.Errorf("email inválido: %w", err)
}
if err := f.validarSenha(senha); err != nil {
return fmt.Errorf("senha inválida: %w", err)
}
fmt.Println("Cadastro realizado com sucesso!")
return nil
}
func main() {
naoVazio := func(s string) error {
if len(s) == 0 {
return fmt.Errorf("não pode ser vazio")
}
return nil
}
validarSenhaForte := func(s string) error {
if len(s) < 8 {
return fmt.Errorf("mínimo de 8 caracteres")
}
return nil
}
form := novoFormulario(naoVazio, validarSenhaForte)
form.Submeter("user@exemplo.com", "senhaforte123")
form.Submeter("", "123")
}
Comparativo: herança vs composição
| Aspecto | Herança | Composição em Go |
|---|---|---|
| Acoplamento | Alto — subclasse depende da base | Baixo — tipos independentes |
| Flexibilidade | Rígida — hierarquia definida cedo | Flexível — combinações livres |
| Polimorfismo | Por hierarquia de tipos | Por interfaces implícitas |
| Reutilização | Vertical — ao longo da hierarquia | Horizontal — qualquer combinação |
| Testabilidade | Difícil — dependências ocultas | Fácil — injeção de dependência natural |
| Evolução | Mudanças na base afetam tudo | Tipos evoluem independentemente |
Go troca a pergunta "de quem este tipo herda?" por duas mais simples: "do que ele é feito?", respondida por embedding, e "o que ele sabe fazer?", respondida por interfaces. A diferença aparece nos detalhes: métodos promovidos não são virtuais, então comportamento compartilhado que depende do tipo concreto vive em funções sobre interfaces; e tudo o que se embute num tipo exportado passa a fazer parte da API dele. Com isso em mente, embedding vira atalho de delegação e interface vira contrato, cada um no seu papel.
Fontes e leituras recomendadas
-
Effective Go — Embedding — Discussão oficial sobre composição por embedding. https://go.dev/doc/effective_go#embedding
-
Go by Example: Interfaces — Base prática para composição via interfaces. https://gobyexample.com/interfaces
-
Go Blog: Composition over inheritance — Artigo da comunidade sobre o modelo de design do Go. https://go.dev/doc/faq#Is_Go_an_object-oriented_language
-
Design Patterns in Go — Decorator — Implementação do padrão Decorator em Go. https://refactoring.guru/design-patterns/decorator/go/example
-
SOLID principles in Go — Como os princípios SOLID se aplicam ao design em Go. https://dave.cheney.net/2016/08/20/solid-go-design
-
Practical Go — Real world advice — Dave Cheney sobre design idiomático em Go. https://dave.cheney.net/practical-go/presentations/gophercon-singapore-2019.html
Exercícios
Exercício 1
No exemplo de Animal, Cachorro e Gato, a correção do artigo foi reescrever Apresentar() em cada tipo. Com vinte animais, isso vira vinte cópias do mesmo código. Por que o Apresentar() promovido não enxerga o Falar() do tipo externo, e como obter o comportamento desejado escrevendo Apresentar uma vez só?
Ver resposta
✓ Resposta: Porque o receiver de Apresentar() é o Animal embutido, e não o Cachorro. Quando se chama c.Apresentar(), o Go traduz para c.Animal.Apresentar(), e lá dentro a.Falar() só pode ser Animal.Falar: o Animal não sabe que está dentro de um Cachorro. Não há despacho virtual no embedding, e é isso que o artigo mostra ao obter Rex diz: .... A forma de escrever o comportamento uma vez é tirá-lo do tipo e pô-lo numa função que recebe a interface: com type Falante interface{ Nome() string; Falar() string }, a função func Apresentar(f Falante) { fmt.Printf("%s diz: %s\n", f.Nome(), f.Falar()) } atende qualquer animal, porque a chamada f.Falar() passa pela interface e resolve para o método do tipo concreto. O Nome() pode vir promovido do Animal, que continua útil para o que não depende de nada sobrescrito; só é preciso renomear o campo para nome, minúsculo, porque um campo e um método não podem se chamar Nome na mesma struct (é o erro do exercício seguinte). É o padrão idiomático em Go: comportamento compartilhado vive em funções sobre interfaces, não em métodos de uma "classe base".
Exercício 2
No exemplo de pedidos, o tipo Desconto tem um campo chamado Descricao_, com sublinhado no fim. Um revisor pede para renomear para Descricao, e o código deixa de compilar. Qual é o erro, e qual seria um nome idiomático para o campo?
Ver resposta
✓ Resposta: Uma struct não pode ter campo e método com o mesmo nome, porque os dois disputariam o seletor d.Descricao. O compilador recusa com field and method with the same name Descricao e aponta a outra declaração. O autor do exemplo contornou o conflito com o sublinhado, mas nomes com _ fogem da convenção do Go, que usa MixedCaps e reserva o sublinhado para quase nada. Como o método Descricao() é exigido pela interface Descritivel, quem muda é o campo, e o nome deve dizer o que ele guarda: Rotulo, Cupom ou Texto. Com Cupom string, o método fica return fmt.Sprintf("%s (%.0f%%)", d.Cupom, d.Percentual), e quem lê entende o dado sem precisar decifrar a exceção. Vale o mesmo para getters: em Go, o campo é nome e o método é Nome(), nunca GetNome().
Exercício 3
Um Servico embute *Logger, um ponteiro, para compartilhar o mesmo logger entre vários serviços. Em um teste, alguém cria var s Servico e chama s.Log("x"). O que acontece, e por que o embedding de ponteiro exige um cuidado que o embedding de valor não exige?
Ver resposta
✓ Resposta: O programa para com invalid memory address or nil pointer dereference. O valor zero de Servico tem o campo embutido *Logger valendo nil. A chamada promovida s.Log vira s.Logger.Log, que recebe um receptor nulo, e o método quebra ao ler l.Prefixo. Com Logger embutido por valor, o valor zero seria um logger utilizável, só com prefixo vazio, e nada quebraria. O embedding de ponteiro troca essa segurança pelo compartilhamento, e a troca pode valer a pena, mas então o tipo precisa de um construtor, como NovoServico(l *Logger) *Servico, que garanta o campo preenchido. Outra saída é o método aceitar receptor nulo, como no exercício da lista encadeada do artigo de ponteiros: if l == nil { return } no início de Log transforma o logger ausente num logger mudo, o que é conveniente em testes. O importante é que o valor zero da struct, que qualquer var s Servico produz, não seja uma bomba.
Exercício 4
Para contar quantos bytes um programa escreve, sem reimplementar nada além disso, alguém declara type contaEscrita struct { io.Writer; n int } e só o método Write. Explique por que isso funciona como um decorator de uma linha, e o que acontece com os outros métodos do Writer original.
Ver resposta
✓ Resposta: Embutir uma interface numa struct promove todos os métodos dela, que passam a chamar o valor guardado no campo. A struct nasce satisfazendo io.Writer sem nenhum código. Declarar Write no tipo externo sobrescreve o promovido, pela regra do artigo, e dentro dele c.Writer.Write(p) delega ao original antes de somar os bytes. Num teste com &contaEscrita{Writer: os.Stdout}, fmt.Fprintln(cw, "olá") imprime a linha normalmente e deixa cw.n em 5 (quatro bytes de "olá" em UTF-8, mais a quebra de linha). É o mesmo decorator do RepositorioComLog, mas sem precisar reescrever os métodos que não mudam: com uma interface maior, como um repositório de dez métodos, o tipo externo sobrescreve só o que quer interceptar e herda o resto pelo embedding. O cuidado é o mesmo do exercício anterior: com o campo da interface vazio, qualquer método não sobrescrito entra em pânico ao ser chamado.
Exercício 5
Uma struct exportada Cache embute sync.Mutex para escrever c.Lock() em vez de c.mu.Lock(). Um usuário da biblioteca, vendo Lock e Unlock na documentação, passa a travar o cache por conta própria antes de chamar os métodos dele, e o programa congela. O que o embedding fez com a API, e qual é a forma recomendada?
Ver resposta
✓ Resposta: O embedding promove os métodos do tipo embutido para a API pública do tipo externo. Com sync.Mutex embutido, Lock e Unlock passam a fazer parte do contrato de Cache: aparecem na documentação, qualquer pacote pode chamá-los, e o autor não consegue mais trocar o mutex por outra coisa sem quebrar quem os usa. O congelamento do exemplo é consequência direta: o usuário trava o mutex, chama um método do cache que tenta travar o mesmo mutex, e como o sync.Mutex do Go não é reentrante, a goroutine espera por si mesma para sempre. A forma recomendada é um campo com nome e minúsculo, mu sync.Mutex, que mantém a sincronização como detalhe de implementação. A regra geral vale para qualquer embedding em tipo exportado: embuta só o que você quer que faça parte da API. Quando a intenção é só reaproveitar código internamente, um campo nomeado com delegação explícita comunica isso melhor.