Pular para o conteúdo

Generics em Go

Publicada em · 21 min de leitura

Tamanho da fonte:
Largura da coluna:

Por mais de uma década, a ausência de tipos genéricos foi a característica mais debatida e controversa do Go. Enquanto comunidades de linguagens vizinhas apontavam a duplicação de funções numéricas e o uso inseguro de interface{} como limitações arcaicas, a equipe de design do Go resistiu pacientemente. O objetivo dos criadores não era impedir abstrações, mas evitar a armadilha do inchaço de compilação do C++, o apagamento de tipos com alocação dinâmica do Java e a perda da simplicidade e velocidade de compilação que tornaram o Go lendário.

Com o lançamento do Go 1.18 em março de 2022, os genéricos chegaram à linguagem através de uma proposta matemática refinada: Type Sets (conjuntos de tipos) e uma estratégia engenhosa de compilação chamada com dicionários de runtime. Esta aula ensina a sintaxe completa de parâmetros de tipo, o mecanismo do operador de aproximação (~), a inferência estática do compilador, os pacotes canônicos introduzidos no Go 1.21 (slices, maps, cmp) e as diretrizes definitivas de quando usar e quando evitar tipos genéricos na arquitetura de seus sistemas.

ParteSeçõesO que você vai aprender
1. A história e o dilema1 e 2O trilema dos genéricos, o custo de interface{} e a chegada no Go 1.18
2. Sintaxe fundamental3 e 4Parâmetros de tipo, structs genéricas (pilhas, filas e caches) e funções utilitárias
3. Type sets e restrições5 a 7Interfaces como conjuntos de tipos, comparable, o operador união (|) e o til (~)
4. Inferência e compilação8 e 9Inferência de argumentos, unificação de tipos e casos com anotação obrigatória
5. Internals e performance10 e 11GcShape Stenciling, dicionários de runtime e comparação de velocidade
6. A biblioteca moderna e iteradores12 a 15Pacotes slices, maps, cmp, iteradores (iter.Seq) no Go 1.23+ e type aliases no Go 1.24+
7. Limitações e fechamento16 a 20Métodos genéricos proibidos, árvore de decisão, exercícios práticos e glossário

Pré-requisitos recomendados: Go, Structs e injeção de dependência.

Parte 1: A história e o dilema

1. O problema antes dos genéricos

Antes do Go 1.18, se você precisasse de uma função utilitária para retornar o menor valor entre dois números inteiros, escrevia:

func MinInt(a, b int) int {
    if a < b {
        return a
    }
    return b
}

No dia seguinte, ao calcular a menor coordenada geográfica em um mapa com números de ponto flutuante, você era obrigado a duplicar o algoritmo:

func MinFloat(a, b float64) float64 {
    if a < b {
        return a
    }
    return b
}

Para coleções genéricas (como fatias, pilhas ou árvores binárias), a alternativa disponível na linguagem era recorrer a interface{} (rebatizada para any no Go 1.18):

// Abordagem legada frágil: perda total de checagem em compilação
func ProcessarItens(itens []any) {
    for _, item := range itens {
        // Exige type assertion manual em runtime
        num, ok := item.(int)
        if !ok {
            panic("esperava um inteiro!") // Erro que só estoura em produção
        }
        _ = num
    }
}

Essa solução dinâmica trazia duas desvantagens críticas:

  1. Perda de segurança estática: O compilador não podia impedir que uma string fosse inserida no meio de uma lista de inteiros, transferindo o risco de pânico para o ambiente de produção.
  2. Degradação de desempenho (): Atribuir um valor primitivo como int a uma interface vazia força o runtime a alocar o valor na memória heap (processo de boxing), gerando sobrecarga de ponteiros e pressão no coletor de lixo.
problema antes dos generics
figura 1 · problema antes dos generics

O diagrama exibe as três eras dessa evolução. No primeiro grupo, a duplicação de código gerava manutenção cara. No segundo grupo, o uso de interface{} causava alocações dinâmicas na heap e checagens manuais de tipo suscetíveis a panics. No terceiro grupo, os parâmetros de tipo (Min[T cmp.Ordered]) unificam o algoritmo em uma única declaração com validação estática e zero sobrecarga de boxing.

2. O trilema dos genéricos na computação

Por que os criadores do Go demoraram mais de uma década para incorporar uma funcionalidade tão comum? A resposta repousa em um dilema conhecido da ciência da computação:

  1. Velocidade de compilação: O compilador deve gerar o binário de forma quase instantânea.
  2. Velocidade de execução: O binário gerado deve rodar com desempenho máximo em código de máquina sem sobrecarga de indirection.
  3. Expressividade e simplicidade do programador: A sintaxe deve ser flexível sem introduzir complexidade indecifrável.

Historicamente, as linguagens escolheram dois desses vértices:

  • C++ (Monomorfização total via templates): Gera código de máquina ultraveloz especializando uma cópia da função para cada tipo, mas sofre com tempo de compilação lento e inchaço massivo do binário (binary bloat).
  • Java (Type Erasure / Apagamento de tipos): Compila rápido e mantém o bytecode enxuto, mas converte tudo para ponteiros genéricos de objetos, pagando um preço severo em alocação de memória e desempenho em tempo de execução.

A equipe do Go recusou-se a aceitar qualquer um desses extremos. Eles esperaram até projetar um meio-termo engenhoso, combinando especialização estática para tipos de valor e compartilhamento de stencils para ponteiros através de dicionários de runtime.

Parte 2: Sintaxe fundamental

3. Parâmetros de tipo e a restrição any

A declaração de uma função genérica em Go utiliza colchetes imediatamente após o nome da função para introduzir parâmetros de tipo:

package main

import "fmt"

// T é o parâmetro de tipo; any é a restrição que permite qualquer tipo do Go
func PrimeiroElemento[T any](lista []T) (T, bool) {
    if len(lista) == 0 {
        var zero T // valor zero do tipo T
        return zero, false
    }
    return lista[0], true
}

func main() {
    numeros := []int{10, 20, 30}
    textos := []string{"Go", "Rust", "C"}

    // O compilador infere o tipo T automaticamente
    n, _ := PrimeiroElemento(numeros) // T é int
    s, _ := PrimeiroElemento(textos)  // T é string

    fmt.Printf("Primeiro número: %d, Primeiro texto: %s\n", n, s)
}

O identificador T é uma convenção para indicar Type, mas você pode usar qualquer nome expressivo, como Elemento, Key ou Value. O identificador any é um alias nativo da linguagem para interface{}.

anatomia type parameters
figura 2 · anatomia type parameters

A figura detalha os blocos fundamentais de uma função genérica:

  • ([T]): O espaço reservado para o tipo concreto.
  • (comparable): O contrato que restringe quais tipos são autorizados a preencher o parâmetro.
  • explícito (Encontrar[string](...)): A passagem manual do tipo na chamada, usada quando a inferência não é possível.
  • Type Inference: A capacidade do compilador de deduzir automaticamente o tipo a partir dos valores repassados nos argumentos de função.

4. Estruturas de dados genéricas

Antes do Go 1.18, implementar uma fila, pilha ou cache genérico em Go exigia pacotes de terceiros com ponteiros unsafe ou alocações de any. Com parâmetros de tipo, podemos criar tipos de dados seguros e limpos:

package collections

import "sync"

// Pilha genérica que opera com qualquer tipo T
type Stack[T any] struct {
    elements []T
    mu       sync.Mutex
}

func NewStack[T any]() *Stack[T] {
    return &Stack[T]{
        elements: make([]T, 0),
    }
}

func (s *Stack[T]) Push(item T) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.elements = append(s.elements, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()

    if len(s.elements) == 0 {
        var zero T
        return zero, false
    }

    lastIdx := len(s.elements) - 1
    item := s.elements[lastIdx]
    s.elements = s.elements[:lastIdx]
    return item, true
}

Ao instanciar a pilha, o tipo desejado é fornecido nos colchetes:

pilhaInt := collections.NewStack[int]()
pilhaInt.Push(42)

pilhaNome := collections.NewStack[string]()
pilhaNome.Push("Aurora")

O compilador rejeita estaticamente a tentativa de inserir uma string em pilhaInt, eliminando a necessidade de conferências defensivas em runtime.

Parte 3: Type sets e restrições

5. A virada conceitual: interfaces como conjuntos de tipos

Antes do Go 1.18, uma interface era compreendida estritamente como uma lista de métodos (Method Set). Se uma struct implementasse todos os métodos exigidos, ela satisfazia a interface.

Com a chegada dos genéricos, a especificação da linguagem expandiu formalmente o significado de interface: uma interface agora define um permitidos (Type Set). Métodos continuam válidos, mas agora uma interface também pode enumerar tipos concretos primitivos utilizando operadores de união.

// Interface que define um conjunto de tipos numéricos permitidos
type Numerico interface {
    int | int32 | int64 | float32 | float64
}

// A função só aceita tipos que pertençam ao conjunto definido na interface
func Somar[T Numerico](a, b T) T {
    return a + b
}

Dentro da função Somar, o operador + torna-se legalmente disponível porque ele é uma operação válida em todos os tipos que compõem o conjunto da interface Numerico.

6. O operador de aproximação til (~)

Imagine que você declare um tipo de domínio customizado para representar um identificador de conta ou um valor monetário:

type Saldo float64

Se você tentar passar uma variável do tipo Saldo para a função Somar declarada acima, o compilador emitirá um erro de compilação:

Saldo does not implement Numerico (possibly missing ~ for float64 in constraint Numerico)

Isso ocorre porque, para o Go, Saldo e float64 são tipos distintos. Para instruir o compilador a aceitar não apenas o tipo exato, mas qualquer tipo definido cujo tipo subjacente (underlying type) seja aquele primitivo, utilizamos o operador de aproximação til (~):

type NumericoAproximado interface {
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}
type sets e aproximacao
figura 3 · type sets e aproximacao

O diagrama demonstra a validação do conjunto de tipos. A interface compõe uma união de tipos com o operador til (~int | ~float64). O compilador testa os tipos candidatos do código: int primitivo é aceito; type MeuInt int e type ID int são aceitos porque seus tipos subjacentes são int. Se a interface tivesse omitido o til (int), os tipos de domínio customizados seriam rejeitados com erro de compilação.

7. As restrições canônicas: comparable e cmp.Ordered

A biblioteca padrão fornece duas restrições universais de altíssima relevância:

  1. comparable: Identificador reservado nativo que representa todos os tipos que suportam os operadores de igualdade e desigualdade (== e !=). Isso engloba inteiros, floats, strings, booleans, ponteiros, canais, structs compostas por campos comparáveis e interfaces. É a restrição obrigatória para chaves de mapas genéricos:
// K precisa ser comparable para servir como chave de mapa
type Cache[K comparable, V any] struct {
    data map[K]V
}
  1. cmp.Ordered (introduzido no pacote cmp do Go 1.21): Restrição que abrange todos os tipos que suportam operadores de ordem (<, <=, >, >=), abrangendo todos os inteiros, pontos flutuantes e strings:
import "cmp"

func Menor[T cmp.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

Parte 4: Inferência e compilação

8. Os dois passos da inferência de tipos

Para manter o código enxuto, o compilador do Go implementa dois mecanismos sofisticados de dedução:

  1. Inferência por argumento de função (Function Argument Inference): O compilador analisa os tipos dos parâmetros concretos passados na chamada e deduz automaticamente os argumentos de tipo equivalentes:
func Imprimir[T any](item T) {
    fmt.Println(item)
}

// O programador não precisa escrever Imprimir[string]("texto")
Imprimir("texto") // O compilador infere T = string
  1. Inferência por restrição de tipo (Constraint Type Inference): Ocorre quando uma restrição expressa uma relação entre múltiplos parâmetros de tipo. Por exemplo, converter uma fatia de elementos em um tipo customizado:
func DuplicarValores[S ~[]E, E ~int](s S) S {
    resultado := make(S, len(s))
    for i, v := range s {
        resultado[i] = v * 2
    }
    return resultado
}

Ao chamar DuplicarValores(meuSlice), o compilador descobre o tipo S a partir da fatia passada e, em seguida, deduz que E é o tipo do elemento contido nessa fatia sem que você precise declará-lo explicitamente.

9. Quando a anotação explícita é obrigatória

Existem situações em que a inferência do compilador não tem informações suficientes para deduzir o tipo, exigindo a passagem manual entre colchetes:

  • Instanciação de structs genéricas: Structs não possuem argumentos de função para guiar o compilador:
// Erro de sintaxe: o compilador exige a passagem do tipo concreto
// p := Stack{} 

p := Stack[int]{} // Correto
  • Funções onde o tipo genérico aparece apenas no retorno:
func NovoItem[T any]() T {
    var item T
    return item
}

// Chamada inválida: NovoItem() -> O compilador não sabe o que retornar
valor := NovoItem[string]() // Correto: informamos explicitamente que T é string

Parte 5: Internals e performance

10. GcShape Stenciling e dicionários de runtime

A maior inovação do Go na implementação de genéricos foi evitar os defeitos de monomorfização excessiva do C++ e do apagamento de tipos do Java. A estratégia empregada pelo compilador chama-se GcShape Stenciling.

Para fins de alocação de memória e coleta de lixo, todos os ponteiros possuem exatamente o mesmo formato físico: uma palavra de máquina de 64 bits (em arquiteturas x86-64 ou ARM64) contendo um endereço de memória que o garbage collector deve rastrear. Portanto, um ponteiro *User, um ponteiro *Order e um ponteiro *string compartilham o mesmo layout de GC (mesmo GC Shape).

gc shape stenciling
figura 4 · gc shape stenciling

O diagrama ilustra o mecanismo de compilação:

  1. Ponteiros agrupados: Ao instanciar Cache[*User] e Cache[*Order], o compilador detecta que ambos compartilham o formato unsafe.Pointer. Em vez de gerar duas rotinas duplicadas em assembly, ele gera um único bloco de código de máquina (stencil).
  2. Dicionário em runtime: Para que o código unificado saiba executar métodos específicos da struct original, o compilador injeta secretamente um argumento extra nas chamadas: um ponteiro para um dicionário de tipos (.dict). Esse dicionário informa o tamanho exato e as tabelas de métodos em tempo de execução.
  3. Especialização para tipos de valor: Para tipos primitivos escalares como int e string, o tamanho e os registradores de CPU são fisicamente diferentes. Para esses tipos, o compilador gera stencils de código especializados e dedicados.

Essa abordagem híbrida garante que o binário final gerado em Go não sofra inchaço (binary bloat) ao criar centenas de tipos genéricos com ponteiros, preservando a rapidez lendária do comando go build.

11. Comparativo de desempenho em números reais

Muitos desenvolvedores questionam se os genéricos em Go introduzem lentidão quando comparados ao código não-genérico ou ao uso de interfaces vazias.

Testes de benchmark na biblioteca padrão revelam o seguinte comportamento:

  • Tipos de valor escalares (int, float64): Uma função genérica monomorfizada executa exatamente na mesma velocidade de uma função concreta dedicada. Não há sobrecarga de ponteiros nem chamadas indiretas.
  • Ponteiros via GcShape: A invocação de métodos dinâmicos a partir do dicionário de runtime adiciona uma sobrecarga mínima de indirection (menos de 2 nanossegundos por chamada em processadores modernos).
  • Comparação contra any / interface{}: O código genérico é de duas a quatro vezes mais rápido do que o código que usa any, pois elimina completamente as alocações de boxing na heap e dispensa as checagens de type assertion em tempo de execução.

Parte 6: A biblioteca moderna

12. O pacote canônico slices (Go 1.21+)

A partir do Go 1.21, a biblioteca padrão passou a incluir utilitários genéricos de alto desempenho para manipular fatias, tornando obsoletas funções caseiras de busca, ordenação e reversão:

package main

import (
    "cmp"
    "fmt"
    "slices"
)

type Produto struct {
    Nome  string
    Preco float64
}

func main() {
    produtos := []Produto{
        {Nome: "Teclado", Preco: 250.0},
        {Nome: "Mouse", Preco: 120.0},
        {Nome: "Monitor", Preco: 900.0},
    }

    // Ordenação customizada estável usando cmp.Compare
    slices.SortFunc(produtos, func(a, b Produto) int {
        return cmp.Compare(a.Preco, b.Preco)
    })

    fmt.Printf("Produto mais barato: %s (R$ %.2f)\n", produtos[0].Nome, produtos[0].Preco)

    // Busca binária rápida em fatias já ordenadas
    idx, achou := slices.BinarySearchFunc(produtos, 120.0, func(p Produto, target float64) int {
        return cmp.Compare(p.Preco, target)
    })

    if achou {
        fmt.Printf("Encontrado no índice %d: %s\n", idx, produtos[idx].Nome)
    }
}

13. O pacote canônico maps (Go 1.21+)

De forma complementar, o pacote maps fornece rotinas padronizadas para clonagem, extração de chaves e limpeza de mapas:

package main

import (
    "fmt"
    "maps"
)

func main() {
    original := map[string]int{"A": 1, "B": 2, "C": 3}

    // Cria uma cópia independente na memória (shallow copy)
    copia := maps.Clone(original)

    // Remove elementos baseado em um predicado
    maps.DeleteFunc(copia, func(k string, v int) bool {
        return v%2 == 0 // apaga pares com valores pares
    })

    fmt.Printf("Original: %v\n", original)
    fmt.Printf("Filtrado: %v\n", copia)
}

14. Iteradores genéricos e : a evolução do Go 1.23+

Quando o Go 1.18 introduziu tipos genéricos, desenvolvedores acostumados com ecossistemas funcionais rapidamente criaram bibliotecas utilitárias externas para prover funções como Map, Filter e Reduce. Embora essas abstrações funcionassem perfeitamente em termos de checagem estática, elas introduziam uma grave ineficiência de memória: a avaliação gulosa (eager).

Se você aplicasse um filtro e depois um mapeamento sobre uma lista contendo 100.000 itens, o código criava duas fatias completas e intermediárias na memória Heap (make([]U, len(itens))), sobrecarregando o Garbage Collector com lixo efêmero. Além disso, cada pacote do ecossistema adotava uma assinatura incompatível de iteração.

A partir do Go 1.23, a linguagem resolveu esse problema de forma canônica ao introduzir iteradores formais no pacote padrão iter e estender o laço for ... := range para suportar funções de iteração nativamente (recurso conhecido como range-over-func).

A assinatura dos tipos canônicos de iteração

No pacote iter, o Go padronizou duas definições fundamentais:

// Iterador de elemento único (ex: valores de uma fatia ou conjunto)
type Seq[V any] func(yield func(V) bool)

// Iterador de pares (ex: chave-valor de mapas ou índice-valor de fatias)
type Seq2[K, V any] func(yield func(K, V) bool)

A função iteradora recebe uma função de retorno chamada convencionalmente de yield. Cada vez que o laço de repetição avança, o iterador invoca yield(valor). Se yield retornar false, significa que o consumidor executou um break ou retornou antecipadamente, e o iterador encerra imediatamente o processamento.

O antes e o depois: pipelines gulosos versus pipelines preguiçosos

O antes (Go 1.18 a 1.22):

// Abordagem legada: aloca uma fatia intermediaria inteira na Heap a cada passo
func MapearGuloso[T any, U any](itens []T, transformador func(T) U) []U {
    resultado := make([]U, len(itens)) // Alocacao na Heap proporcional ao tamanho
    for i, v := range itens {
        resultado[i] = transformador(v)
    }
    return resultado
}

O depois (Go 1.23+):

// Abordagem moderna: execucao sob demanda (lazy) com zero alocacao intermediaria
package main

import (
    "fmt"
    "iter"
    "slices"
    "strings"
)

// MapearIter transforma elementos sem criar nenhum slice intermediario
func MapearIter[T any, U any](seq iter.Seq[T], transformador func(T) U) iter.Seq[U] {
    return func(yield func(U) bool) {
        for item := range seq {
            // Se o corpo do loop externo der break, yield retorna false e abortamos
            if !yield(transformador(item)) {
                return
            }
        }
    }
}

func main() {
    nomes := []string{"golang", "concorrencia", "generics", "iteradores"}

    // slices.Values (Go 1.23+) converte a fatia em um iterador iter.Seq[string]
    iteradorEmMaiusculo := MapearIter(slices.Values(nomes), strings.ToUpper)

    // O laco consome os dados elemento a elemento diretamente pelo range nativo
    for valor := range iteradorEmMaiusculo {
        fmt.Println(valor)
    }

    // Se no final for necessario consolidar em uma fatia, use slices.Collect
    resultadoFinal := slices.Collect(iteradorEmMaiusculo)
    fmt.Printf("Total coletado na memoria: %d itens\n", len(resultadoFinal))
}

Com o suporte canônico a iter.Seq, os pacotes padrão slices e maps ganharam dezenas de utilitários de alta performance no Go 1.23+, como slices.Values(), slices.All(), slices.Collect(), maps.Keys() e maps.Values(), aposentando dezenas de dependências externas de utilitários funcionais.

15. Type aliases genéricos (Go 1.24+)

Outra lacuna histórica dos genéricos em Go era a impossibilidade de criar apelidos de tipo (type aliases) parametrizados.

Entre as versões Go 1.18 e Go 1.23, se você tentasse escrever um alias de tipo com parâmetros genéricos, o compilador abortava imediatamente com erro de sintaxe:

// Invalido no Go 1.18 a 1.23:
// type Conjunto[T comparable] = map[T]struct{} 
// Erro de compilacao: cannot use generic type in alias

A única alternativa tolerada era criar uma definição de novo tipo (type Conjunto[T comparable] map[T]struct{}). Porém, ao criar um novo tipo em vez de um alias, o Go considerava os tipos semanticamente distintos, exigindo conversões explícitas e dolorosas sempre que dados precisavam fluir entre bibliotecas ou pacotes refatorados.

A partir do Go 1.24, os aliases genéricos de tipo são cidadãos de primeira classe na linguagem.

O antes (Go 1.18 a 1.23):

// Antes: Novo tipo definido (nao e alias)
type TabelaHash[K comparable, V any] map[K]V

func ProcessarMapa(m map[string]int) {}

func ExemploAntes() {
    var t TabelaHash[string, int] = make(TabelaHash[string, int])
    // Erro de compilacao: TabelaHash e map[string]int sao tipos distintos!
    // ProcessarMapa(t) 
    ProcessarMapa(map[string]int(t)) // Conversao explicita obrigatoria
}

O depois (Go 1.24+):

// Depois: Type Alias Generico com o operador '='
package main

import "fmt"

// TabelaHash e IDENTICA em todos os sentidos a map[K]V
type TabelaHash[K comparable, V any] = map[K]V

func ProcessarMapa(m map[string]int) {
    fmt.Printf("Mapa processado com %d elementos\n", len(m))
}

func main() {
    t := make(TabelaHash[string, int])
    t["requisicoes"] = 42

    // Funciona de forma transparente sem necessidade de conversao de tipo
    ProcessarMapa(t)
}

Esse recurso é indispensável em arquiteturas corporativas e monorepos de grande porte para migrar estruturas genéricas de um pacote para outro sem quebrar a compatibilidade de código de nenhum consumidor existente.

Parte 7: Limitações e fechamento

16. O que você NÃO pode fazer com genéricos em Go

Para evitar complexidade excessiva no compilador, o design dos genéricos em Go introduziu algumas restrições intencionais que todo desenvolvedor deve conhecer:

  1. Métodos de structs não podem declarar parâmetros de tipo próprios: Uma struct genérica pode ter métodos que utilizam os parâmetros de tipo da struct, mas um método individual não pode introduzir novos tipos genéricos:
type MinhaStruct[T any] struct{}

// VÁLIDO: utiliza o parâmetro T definido na struct
func (m *MinhaStruct[T]) Processar(item T) {}

// INVÁLIDO: Go proíbe métodos de introduzir novos parâmetros de tipo [U any]
// func (m *MinhaStruct[T]) Converter[U any](item U) {}

A solução canônica para esse cenário é transformar a operação em uma função genérica de pacote em vez de método:

// Forma correta: função comum que recebe múltiplos tipos genéricos
func Converter[T any, U any](m *MinhaStruct[T], item U) {}
  1. Tipos genéricos não podem ser usados como discriminadores de type switch: Você não pode fazer switch v.(type) diretamente sobre uma variável de tipo genérico T.
  2. Ausência de especialização de métodos: Não é possível fornecer uma implementação diferente de um método para Stack[int] e outra para Stack[string].

17. Árvore de decisão: quando usar genéricos

Um dos maiores perigos ao aprender genéricos é o entusiasmo excessivo: tentar reescrever todas as interfaces e structs da sua aplicação com parâmetros de tipo.

arvore decisao generics
figura 5 · arvore decisao generics

A árvore de decisão resume os critérios práticos recomendados pelo time central do Go:

  • Use Generics quando: O algoritmo opera puramente sobre a estrutura dos dados e a lógica é idêntica para qualquer tipo (pilhas, filas, árvores, caches, algoritmos de ordenação, transformações de slice como Map/Filter/Reduce).
  • Use Interfaces clássicas quando: O comportamento varia dinamicamente em tempo de execução e você precisa de polimorfismo arquitetural (ex: repositórios de banco de dados, io.Reader, http.Handler).
  • Use Tipos Concretos quando: A regra lida com conceitos específicos de negócio (serviços de pagamento, regras fiscais, emissão de faturas). A clareza de um tipo concreto supera qualquer abstração desnecessária.

18. Exercícios práticos

Exercício 1: Função genérica Map para slices

Cenário: Escreva uma função genérica Mapear[T any, U any](itens []T, transformador func(T) U) []U que receba uma fatia de qualquer tipo e retorne uma nova fatia com os elementos convertidos para outro tipo.

Solução comentada:

package main

import (
    "fmt"
    "strconv"
)

func Mapear[T any, U any](itens []T, transformador func(T) U) []U {
    resultado := make([]U, len(itens))
    for i, v := range itens {
        resultado[i] = transformador(v)
    }
    return resultado
}

func main() {
    numeros := []int{10, 20, 30}
    textos := Mapear(numeros, func(n int) string {
        return "R$ " + strconv.Itoa(n)
    })
    fmt.Printf("%v\n", textos) // [R$ 10, R$ 20, R$ 30]
}

Nota de evolução (Go 1.23+): Enquanto a implementação gulosa acima é excelente para entender a mecânica de múltiplos parâmetros de tipo (T e U), em sistemas de alta vazão ou listas volumosas prefira construir iteradores sob demanda com iter.Seq[U]. Dessa forma, os elementos são gerados e consumidos preguiçosamente, eliminando a alocação de fatias intermediárias na Heap.

Exercício 2: Restrição de tipo com operador til

Cenário: Crie uma restrição chamada Identificador que aceite o tipo primitivo string ou qualquer tipo customizado de domínio baseado em string. Implemente uma função Contem[T Identificador](lista []T, alvo T) bool.

Solução comentada:

package main

import "fmt"

type Identificador interface {
    ~string
}

type UserID string

func Contem[T Identificador](lista []T, alvo T) bool {
    for _, item := range lista {
        if item == alvo {
            return true
        }
    }
    return false
}

func main() {
    usuarios := []UserID{"usr-1", "usr-2", "usr-3"}
    fmt.Println(Contem(usuarios, UserID("usr-2"))) // true
    fmt.Println(Contem(usuarios, UserID("usr-9"))) // false
}

Exercício 3: Conjunto de elementos únicos (Set genérico)

Cenário: Implemente uma estrutura de dados de Conjunto (Set[T comparable]) com métodos Add(item T), Contains(item T) bool e Elements() []T.

Solução comentada:

package collections

type Set[T comparable] struct {
    elements map[T]struct{}
}

func NewSet[T comparable]() *Set[T] {
    return &Set[T]{
        elements: make(map[T]struct{}),
    }
}

func (s *Set[T]) Add(item T) {
    s.elements[item] = struct{}{}
}

func (s *Set[T]) Contains(item T) bool {
    _, exists := s.elements[item]
    return exists
}

func (s *Set[T]) Elements() []T {
    resultado := make([]T, 0, len(s.elements))
    for k := range s.elements {
        resultado = append(resultado, k)
    }
    return resultado
}

Exercício 4: Algoritmo Min/Max com cmp.Ordered

Cenário: Implemente uma função Limitar[T cmp.Ordered](valor, min, max T) T que prenda um valor entre limites mínimo e máximo estipulados.

Solução comentada:

package main

import (
    "cmp"
    "fmt"
)

func Limitar[T cmp.Ordered](valor, min, max T) T {
    if valor < min {
        return min
    }
    if valor > max {
        return max
    }
    return valor
}

func main() {
    fmt.Println(Limitar(15, 0, 10))   // 10
    fmt.Println(Limitar(-5, 0, 10))   // 0
    fmt.Println(Limitar(7.5, 0.0, 10.0)) // 7.5
}

Exercício 5: Refatoração de interface{} para genéricos

Cenário: O código a seguir utiliza any para encontrar o índice de um item em uma fatia, provocando alocações na heap. Refatore a função para usar genéricos com a restrição adequada.

// Código legado com alocação e perda de tipagem
func IndiceDe(itens []any, alvo any) int {
    for i, v := range itens {
        if v == alvo {
            return i
        }
    }
    return -1
}

Solução comentada:

package utils

// Utiliza a restrição comparable para habilitar o operador == com tipagem estática
func IndiceDe[T comparable](itens []T, alvo T) int {
    for i, v := range itens {
        if v == alvo {
            return i
        }
    }
    return -1
}

19. Glossário técnico

  • Type Parameter (Parâmetro de Tipo): Identificador declarado entre colchetes em funções ou structs que atua como espaço reservado para um tipo concreto.
  • Type Argument (Argumento de Tipo): O tipo concreto informado na chamada ou inferido pelo compilador para preencher o parâmetro de tipo.
  • Type Constraint (Restrição de Tipo): A interface que limita o conjunto de tipos aceitos por um parâmetro de tipo.
  • Type Set (Conjunto de Tipos): A definição matemática moderna de uma interface em Go como um conjunto finito ou infinito de tipos permitidos.
  • Approximation Element (~T): O operador til que faz a restrição casar com qualquer tipo cujo tipo subjacente seja T.
  • iter.Seq / iter.Seq2: Tipos funcionais canônicos introduzidos no Go 1.23 para suportar sequências de valores ou pares sem alocação intermediária.
  • Range-over-func: Capacidade da linguagem (Go 1.23+) de iterar nativamente com a instrução for ... := range sobre funções com assinatura iter.Seq ou iter.Seq2.
  • Generic Type Alias: Recurso introduzido no Go 1.24 que permite declarar apelidos de tipo com parâmetros de tipo (type Alias[T any] = Original[T]), preservando a identidade exata do tipo.
  • GcShape Stenciling: Técnica do compilador do Go que agrupa tipos com layouts de memória idênticos para gerar código de máquina compartilhado e evitar inchaço do binário.
  • Runtime Dictionary: Tabela interna injetada pelo compilador para permitir que código com stencils compartilhados consulte tamanhos e métodos dinâmicos.
  • Boxing: O processo de envolver um valor primitivo dentro de uma casca de interface na memória heap, evitado pelo uso de genéricos.

20. Fontes consultadas e leituras recomendadas

  • Go Blog: “An Introduction to Generics”, por Robert Griesemer e Ian Lance Taylor.
  • Go Blog: “When To Use Generics”, por Ian Lance Taylor.
  • Documentação Oficial da Linguagem Go: “Type Parameters Proposal and Language Specification”. Consulta realizada em 18 de outubro de 2026.
  • Pacotes canônicos da biblioteca padrão: slices, maps, cmp e iter (Go 1.21 a Go 1.23+).
  • Especificação oficial da linguagem Go: “Generic type alias declarations (Go 1.24)”.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?