O que é Polymorphism em Go?

Polymorphism (polimorfismo) em Go é a capacidade de tratar valores de tipos diferentes de forma uniforme através de uma interface comum. Diferente de linguagens orientadas a objetos tradicionais que usam hierarquias de classes, Go implementa polimorfismo principalmente através de interfaces implícitas — se um tipo tem os métodos certos, ele satisfaz a interface automaticamente.

Go oferece duas formas principais de polimorfismo:

  1. Polimorfismo em tempo de execução — via interfaces (runtime polymorphism)
  2. Polimorfismo em tempo de compilação — via generics (compile-time polymorphism)

Não existe herança em Go, nem classes, nem keywords como extends ou implements. O polimorfismo em Go é mais simples, mais flexível e mais composível do que em linguagens com herança.

Interface-based polymorphism

O mecanismo central de polimorfismo em Go são as interfaces. Qualquer tipo que implemente os métodos de uma interface satisfaz essa interface implicitamente:

type Forma interface {
    Area() float64
    Perimetro() float64
}

type Circulo struct {
    Raio float64
}

func (c Circulo) Area() float64 {
    return math.Pi * c.Raio * c.Raio
}

func (c Circulo) Perimetro() float64 {
    return 2 * math.Pi * c.Raio
}

type Retangulo struct {
    Largura, Altura float64
}

func (r Retangulo) Area() float64 {
    return r.Largura * r.Altura
}

func (r Retangulo) Perimetro() float64 {
    return 2 * (r.Largura + r.Altura)
}

type Triangulo struct {
    Base, Altura, LadoA, LadoB, LadoC float64
}

func (t Triangulo) Area() float64 {
    return t.Base * t.Altura / 2
}

func (t Triangulo) Perimetro() float64 {
    return t.LadoA + t.LadoB + t.LadoC
}

Agora podemos escrever funções que trabalham com qualquer Forma:

func ImprimirInfo(f Forma) {
    fmt.Printf("Área: %.2f | Perímetro: %.2f\n", f.Area(), f.Perimetro())
}

func AreaTotal(formas []Forma) float64 {
    total := 0.0
    for _, f := range formas {
        total += f.Area()
    }
    return total
}

func main() {
    formas := []Forma{
        Circulo{Raio: 5},
        Retangulo{Largura: 10, Altura: 3},
        Triangulo{Base: 8, Altura: 6, LadoA: 8, LadoB: 6, LadoC: 10},
    }

    for _, f := range formas {
        ImprimirInfo(f)
    }

    fmt.Printf("Área total: %.2f\n", AreaTotal(formas))
}

Nenhum dos tipos precisa declarar que implementa Forma. Se tem Area() e Perimetro(), satisfaz a interface. Isso é chamado de duck typing estático — “se anda como pato e faz quack como pato, é um pato”.

Duck typing em Go

O duck typing de Go é verificado em tempo de compilação, diferente do duck typing dinâmico de Python ou Ruby. Isso dá a flexibilidade do duck typing com a segurança de tipos estáticos:

type Salvavel interface {
    Salvar() error
}

// Qualquer tipo que tenha Salvar() error satisfaz Salvavel
type Usuario struct {
    Nome  string
    Email string
}

func (u *Usuario) Salvar() error {
    fmt.Printf("Salvando usuário: %s\n", u.Nome)
    return nil
}

type Configuracao struct {
    Chave string
    Valor string
}

func (c *Configuracao) Salvar() error {
    fmt.Printf("Salvando config: %s=%s\n", c.Chave, c.Valor)
    return nil
}

// SalvarTodos funciona com qualquer tipo que implemente Salvar()
func SalvarTodos(itens []Salvavel) error {
    for _, item := range itens {
        if err := item.Salvar(); err != nil {
            return err
        }
    }
    return nil
}

Essa abordagem facilita criar código desacoplado e testável. É amplamente usada em testes com Go para criar mocks.

Empty interface e any

A interface vazia interface{} (ou seu alias any a partir de Go 1.18) aceita qualquer valor, proporcionando polimorfismo máximo ao custo de segurança de tipos:

func Imprimir(valores ...any) {
    for _, v := range valores {
        fmt.Printf("Tipo: %T, Valor: %v\n", v, v)
    }
}

func main() {
    Imprimir(42, "hello", true, 3.14, []int{1, 2, 3})
}

A interface vazia é útil para funções genéricas como fmt.Println, mas deve ser usada com moderação. Quando possível, prefira interfaces específicas ou generics, que oferecem verificação em tempo de compilação.

Type assertion

Type assertion permite acessar o tipo concreto por trás de uma interface:

func Processar(v interface{}) {
    // Type assertion simples — pode causar panic se errada
    s := v.(string)
    fmt.Println("String:", s)
}

func ProcessarSeguro(v interface{}) {
    // Type assertion com verificação — seguro
    s, ok := v.(string)
    if ok {
        fmt.Println("É string:", s)
        return
    }

    n, ok := v.(int)
    if ok {
        fmt.Println("É int:", n)
        return
    }

    fmt.Println("Tipo desconhecido:", v)
}

Sempre use a forma com dois retornos (valor, ok) para evitar panics em runtime. A forma com um retorno só é segura quando você tem certeza absoluta do tipo.

Type switch

O type switch é a forma idiomática de lidar com múltiplos tipos em Go:

type Resultado interface{}

func FormatarResultado(r Resultado) string {
    switch v := r.(type) {
    case string:
        return fmt.Sprintf("Texto: %s", v)
    case int:
        return fmt.Sprintf("Número: %d", v)
    case float64:
        return fmt.Sprintf("Decimal: %.2f", v)
    case bool:
        if v {
            return "Verdadeiro"
        }
        return "Falso"
    case []byte:
        return fmt.Sprintf("Bytes: %x", v)
    case nil:
        return "Vazio"
    case error:
        return fmt.Sprintf("Erro: %s", v.Error())
    default:
        return fmt.Sprintf("Desconhecido: %T", v)
    }
}

Type switches são muito usados em codecs, serialização, e no tratamento de errors customizados:

func TratarErro(err error) {
    switch e := err.(type) {
    case *json.SyntaxError:
        fmt.Printf("JSON inválido na posição %d\n", e.Offset)
    case *url.Error:
        fmt.Printf("Erro de URL: %s (operação: %s)\n", e.Err, e.Op)
    case *net.OpError:
        fmt.Printf("Erro de rede: %s\n", e.Err)
    default:
        fmt.Printf("Erro genérico: %s\n", err)
    }
}

Generics como polimorfismo em tempo de compilação

A partir de Go 1.18, generics oferecem polimorfismo verificado em tempo de compilação:

// Sem generics — precisa de uma função para cada tipo
func SomaInt(nums []int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func SomaFloat(nums []float64) float64 {
    total := 0.0
    for _, n := range nums {
        total += n
    }
    return total
}

// Com generics — uma função para todos os tipos numéricos
type Numero interface {
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

func Soma[T Numero](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(Soma([]int{1, 2, 3, 4, 5}))       // 15
    fmt.Println(Soma([]float64{1.1, 2.2, 3.3}))     // 6.6
}

Generics são ideais quando a lógica é idêntica para diferentes tipos mas as interfaces não são suficientes. Para mais sobre generics, veja o guia prático de generics em Go.

Interfaces vs generics — quando usar cada

InterfacesGenerics
Comportamento diferente por tipoMesma lógica para múltiplos tipos
Polimorfismo em runtimePolimorfismo em compile-time
Coleções heterogêneas ([]Forma)Coleções homogêneas ([]T)
Performance: indireção (vtable)Performance: sem indireção (monomorphization)
Padrão clássico de GoDisponível a partir de Go 1.18

Strategy pattern em Go

O Strategy pattern é um dos padrões mais naturais em Go graças ao polimorfismo por interfaces:

type Compressao interface {
    Comprimir(dados []byte) ([]byte, error)
    Descomprimir(dados []byte) ([]byte, error)
    Nome() string
}

type GzipCompressao struct{}

func (g GzipCompressao) Comprimir(dados []byte) ([]byte, error) {
    var buf bytes.Buffer
    w := gzip.NewWriter(&buf)
    _, err := w.Write(dados)
    if err != nil {
        return nil, err
    }
    w.Close()
    return buf.Bytes(), nil
}

func (g GzipCompressao) Descomprimir(dados []byte) ([]byte, error) {
    r, err := gzip.NewReader(bytes.NewReader(dados))
    if err != nil {
        return nil, err
    }
    defer r.Close()
    return io.ReadAll(r)
}

func (g GzipCompressao) Nome() string { return "gzip" }

type ArmazenamentoArquivo struct {
    Diretorio  string
    Compressao Compressao // strategy injetada
}

func (a *ArmazenamentoArquivo) Salvar(nome string, dados []byte) error {
    comprimido, err := a.Compressao.Comprimir(dados)
    if err != nil {
        return err
    }
    return os.WriteFile(
        filepath.Join(a.Diretorio, nome+"."+a.Compressao.Nome()),
        comprimido,
        0644,
    )
}

Esse padrão é fundamental em microserviços e em projetos que seguem clean architecture.

Polimorfismo na biblioteca padrão

A biblioteca padrão de Go é repleta de exemplos de polimorfismo por interfaces:

// io.Reader — qualquer fonte de dados
func ProcessarDados(r io.Reader) error {
    dados, err := io.ReadAll(r)
    if err != nil {
        return err
    }
    fmt.Printf("Processados %d bytes\n", len(dados))
    return nil
}

func main() {
    // Todas essas fontes satisfazem io.Reader
    ProcessarDados(strings.NewReader("hello"))         // string
    ProcessarDados(bytes.NewBufferString("world"))      // buffer
    ProcessarDados(os.Stdin)                            // stdin

    f, _ := os.Open("arquivo.txt")
    defer f.Close()
    ProcessarDados(f)                                   // arquivo

    resp, _ := http.Get("https://example.com")
    defer resp.Body.Close()
    ProcessarDados(resp.Body)                           // HTTP response
}

Interfaces como io.Reader, io.Writer, fmt.Stringer e error são exemplos de polimorfismo elegante e minimalista. Veja mais sobre esse padrão na entrada sobre interface e no tutorial de API REST com Go.

Polimorfismo sem herança

Go prova que polimorfismo não precisa de herança. A combinação de interfaces implícitas, composição via embedding e generics oferece tudo que herança oferece — sem os problemas:

// Em Java/C++:
// class Animal { virtual string falar() = 0; }
// class Cachorro extends Animal { string falar() { return "Au!"; } }

// Em Go — mais simples, mais flexível:
type Falante interface {
    Falar() string
}

type Cachorro struct{ Nome string }
func (c Cachorro) Falar() string { return "Au!" }

type Gato struct{ Nome string }
func (g Gato) Falar() string { return "Miau!" }

// Nenhuma relação de herança — apenas comportamento compartilhado
func Saudar(f Falante) {
    fmt.Println(f.Falar())
}

Esse design encoraja interfaces pequenas e focadas, levando a código mais modular e desacoplado. Para entender como Go organiza tipos sem herança, veja as entradas sobre struct e type.

Boas práticas

  1. Interfaces pequenas — prefira interfaces com 1-3 métodos
  2. Aceite interfaces, retorne tipos concretos — assinatura flexível, retorno específico
  3. Defina interfaces no pacote consumidor — não no pacote que implementa
  4. Use generics para lógica idêntica — interfaces para comportamento diferente
  5. Evite interface{} / any — use tipos específicos ou generics quando possível
  6. Type switch com cuidado — geralmente indica que uma interface está faltando

Para mais sobre design de código Go, consulte o tutorial de Go para iniciantes e o guia de concorrência em Go.

Perguntas frequentes (FAQ)

Go suporta polimorfismo sem classes?

Sim. Go implementa polimorfismo através de interfaces implícitas e generics, sem necessidade de classes ou herança. Qualquer tipo que implemente os métodos de uma interface satisfaz essa interface automaticamente. Isso é chamado de “duck typing estático” e oferece flexibilidade semelhante ao duck typing dinâmico com segurança de tipos em tempo de compilação.

Qual a diferença entre polimorfismo por interface e por generics?

Polimorfismo por interface acontece em runtime — diferentes tipos podem ter implementações diferentes do mesmo método, e o tipo concreto é resolvido em tempo de execução. Polimorfismo por generics acontece em compile-time — a mesma lógica é aplicada a diferentes tipos, e o compilador gera código específico para cada tipo. Interfaces são ideais para comportamento variável; generics para lógica idêntica em tipos diferentes.

O que é duck typing em Go?

Duck typing em Go significa que um tipo satisfaz uma interface automaticamente se possui todos os métodos exigidos — sem declaração explícita como implements. Diferente de Python ou Ruby (duck typing dinâmico), Go verifica isso em tempo de compilação (duck typing estático), garantindo segurança de tipos. O nome vem do ditado: “se anda como pato e faz quack como pato, é um pato”.

Quando devo usar type assertion vs type switch?

Use type assertion (v.(Tipo)) quando você espera um tipo específico e quer acessá-lo diretamente. Use type switch (switch v.(type)) quando precisa lidar com múltiplos tipos possíveis. Type switch é mais legível e seguro para múltiplos tipos. Sempre use a forma valor, ok := v.(Tipo) para evitar panics com type assertion.