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() {
import_strings := 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(import_strings, 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 |
Resumo do que foi coberto
Este artigo apresentou a filosofia de composição do Go como substituta da herança: embedding para reutilização de código, interfaces para polimorfismo, delegação explícita para o padrão Decorator, a diferença entre métodos promovidos e métodos virtuais, e injeção de comportamento via funções. Esses padrões juntos formam o núcleo do design idiomático em Go.
Referências e leituras complementares
-
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