Generics: type parameters em Go 1.18+
1. Introdução aos Generics em Go
Generics representam uma das maiores adições à linguagem Go desde sua criação. Introduzidos oficialmente no Go 1.18, os type parameters permitem escrever funções e tipos que podem operar com diferentes tipos concretos sem sacrificar a segurança de tipos em tempo de compilação.
Antes do Go 1.18, desenvolvedores enfrentavam um dilema constante: duplicar código para cada tipo específico ou recorrer a interface{} com type assertions, perdendo a verificação estática de tipos. Generics resolvem esse problema permitindo que você escreva código parametrizado por tipos, mantendo a type safety e eliminando redundâncias.
// Antes: duplicação para cada tipo
func SomaInts(a, b int) int { return a + b }
func SomaFloats(a, b float64) float64 { return a + b }
// Depois: uma única função genérica
func Soma[T int | float64](a, b T) T { return a + b }
2. Sintaxe Básica de Type Parameters
A sintaxe de type parameters utiliza colchetes [] para declarar parâmetros de tipo antes dos parâmetros tradicionais da função.
// Função genérica simples
func Primeiro[T any](slice []T) T {
return slice[0]
}
// Múltiplos type parameters
func Converter[T, U any](entrada T, saida *U) error {
// implementação
return nil
}
// Tipo genérico (struct)
type Pilha[T any] struct {
elementos []T
}
func (p *Pilha[T) Empurrar(item T) {
p.elementos = append(p.elementos, item)
}
func (p *Pilha[T]) Topo() T {
if len(p.elementos) == 0 {
var zero T
return zero
}
return p.elementos[len(p.elementos)-1]
}
A inferência de tipos permite que você omita os type parameters na chamada quando o compilador pode deduzi-los:
numeros := []int{1, 2, 3}
primeiro := Primeiro(numeros) // Inferido como int
// Especificação explícita quando necessário
primeiro = Primeiro[int](numeros)
3. Constraints: Restringindo Type Parameters
Constraints definem quais tipos são permitidos como argumentos para um type parameter. A constraint any é equivalente a interface{} e aceita qualquer tipo.
// Constraint personalizada
type Numerico interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func SomaSlice[T Numerico](slice []T) T {
var total T
for _, v := range slice {
total += v
}
return total
}
Constraints podem incluir métodos, permitindo que você chame métodos nos parâmetros genéricos:
type Stringer interface {
String() string
}
func ImprimirLista[T Stringer](itens []T) {
for _, item := range itens {
fmt.Println(item.String())
}
}
4. Uso de ~ e Tipos Aproximados em Constraints
O operador ~ permite que uma constraint aceite tipos cujo tipo subjacente corresponde ao especificado. Isso é crucial para trabalhar com tipos personalizados baseados em tipos primitivos.
type MeuInt int
type InteiroAproximado interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
func Dobro[T InteiroAproximado](valor T) T {
return valor * 2
}
func main() {
var x MeuInt = 5
fmt.Println(Dobro(x)) // Funciona graças ao ~int
}
Sem o ~, a função Dobro não aceitaria MeuInt, mesmo que seu tipo subjacente seja int. Use com moderação: tipos aproximados podem tornar suas constraints mais flexíveis, mas também mais complexas de entender.
5. Generics com Slices, Maps e Canais
Generics brilham em estruturas de dados e operações utilitárias:
// Filter genérico para slices
func Filter[T any](slice []T, predicate func(T) bool) []T {
resultado := make([]T, 0)
for _, v := range slice {
if predicate(v) {
resultado = append(resultado, v)
}
}
return resultado
}
// Map genérico
func Map[T, U any](slice []T, transform func(T) U) []U {
resultado := make([]U, len(slice))
for i, v := range slice {
resultado[i] = transform(v)
}
return resultado
}
// Operações em maps
func Chaves[K comparable, V any](m map[K]V) []K {
chaves := make([]K, 0, len(m))
for k := range m {
chaves = append(chaves, k)
}
return chaves
}
// Canal genérico com fan-in
func Merge[T any](canais ...<-chan T) <-chan T {
saida := make(chan T)
for _, c := range canais {
go func(ch <-chan T) {
for v := range ch {
saida <- v
}
}(c)
}
return saida
}
6. Type Inference e Limitações Práticas
O compilador Go realiza inferência de tipos na maioria dos casos, mas existem situações onde você precisa especificar explicitamente os type parameters:
// Inferência funciona
nums := []int{1, 2, 3}
dobrados := Map(nums, func(x int) int { return x * 2 })
// Inferência falha - precisa especificar
resultado := Map[int, string](nums, strconv.Itoa)
// Limitação: operadores aritméticos em tipos genéricos
func Soma[T any](a, b T) T {
// return a + b // ERRO: operador + não definido para T
// Solução: usar constraint com operadores
}
Limitações importantes:
- Sem variância: slices de tipos relacionados não são automaticamente compatíveis
- Sem operadores em constraints: você não pode definir constraints que exijam operadores como + ou *
- Sem especialização: não é possível fornecer implementações diferentes para tipos específicos
7. Comparação com Alternativas Pré-1.18
Antes dos generics, as soluções principais eram:
// Abordagem com interface{} (type safety perdida)
func SomaInterface(a, b interface{}) interface{} {
switch v := a.(type) {
case int:
return v + b.(int)
case float64:
return v + b.(float64)
}
return nil
}
// Abordagem com reflection (lenta e verbosa)
func SomaReflection(a, b interface{}) interface{} {
va := reflect.ValueOf(a)
vb := reflect.ValueOf(b)
// implementação complexa...
}
Com generics, você ganha:
- Verificação em tempo de compilação: erros de tipo capturados antes da execução
- Performance: sem overhead de reflection ou type assertions
- Código mais limpo: sem switch/case para cada tipo
- Documentação automática: os tipos são explicitamente declarados
8. Boas Práticas e Padrões com Generics
Use generics quando:
- Você precisa de algoritmos que funcionam independentemente do tipo (sort, filter, map)
- Você está criando estruturas de dados reutilizáveis (pilha, fila, árvore)
- Você quer evitar duplicação de código para tipos similares
Evite generics quando:
- O código se torna mais complexo que a duplicação simples
- Você está lidando com apenas um ou dois tipos específicos
- A interface do problema já é bem modelada com interfaces tradicionais
// Bom uso: estrutura de dados genérica
type ArvoreBinaria[T comparable] struct {
valor T
esq *ArvoreBinaria[T]
dir *ArvoreBinaria[T]
}
// Evitar: complexidade desnecessária
type Resultado[T any] struct {
Valor T
Erro error
}
// Prefira retornar (T, error) diretamente
Testes com generics seguem o padrão normal, mas você pode usar type inference para simplificar:
func TestPilha(t *testing.T) {
pilha := new(Pilha[int])
pilha.Empurrar(1)
pilha.Empurrar(2)
if topo := pilha.Topo(); topo != 2 {
t.Errorf("esperado 2, obtido %d", topo)
}
}
Documente suas funções genéricas explicando quais constraints são necessárias e por quê. Use nomes de type parameters descritivos (T, K, V, E) seguindo as convenções da comunidade.
Generics em Go foram projetados para serem simples e pragmáticos. Eles resolvem problemas reais de reuso de código sem introduzir a complexidade de templates ou metaprogramação encontrada em outras linguagens. Use-os com sabedoria e seu código Go será mais expressivo, seguro e reutilizável.
Referências
- Go 1.18 Release Notes - Generics — Notas oficiais de lançamento detalhando a implementação de generics no Go 1.18
- Tutorial: Getting started with generics — Tutorial oficial da Go Documentation com exemplos práticos introdutórios
- Proposal: Type Parameters in Go — Proposta de design original que motivou a implementação de type parameters
- Go by Example: Generics — Exemplos práticos e concisos de uso de generics em Go
- Understanding Go Generics: Type Parameters and Constraints — Artigo técnico detalhado sobre type parameters, constraints e boas práticas