Introdução aos Generics em Go
Generics é um recurso que chegou ao Go na versão 1.18, permitindo que você escreva código mais flexível e reutilizável sem perder a segurança de tipos. Antes disso, Go era conhecida por ser uma linguagem simples, sem suporte a tipos genéricos, o que frequentemente levava ao uso de interface{} e type assertions — uma abordagem que funcionava, mas era propensa a erros em tempo de execução.
O impacto de adicionar generics ao Go foi significativo: agora você pode escrever funções, tipos e métodos que trabalham com múltiplos tipos de dados mantendo a segurança de tipos em tempo de compilação. Isso reduz duplicação de código, elimina type assertions desnecessários e melhora a legibilidade. Neste artigo, você entenderá como usar type parameters e constraints de forma prática, e verá casos reais onde generics fazem toda diferença.
Type Parameters: O Conceito Fundamental
Type parameters são placeholders para tipos que serão preenchidos quando você usa a função ou tipo genérico. Eles são declarados entre colchetes angulares [] e funcionam de forma semelhante a linguagens como Java e TypeScript, mas com a simplicidade que Go sempre promoveu.
Sintaxe Básica
A sintaxe para declarar um type parameter é simples. Em uma função genérica, você coloca o nome do tipo entre colchetes após o nome da função:
func Primeiro[T any](slice []T) T {
if len(slice) == 0 {
var zero T
return zero
}
return slice[0]
}
Aqui, T é o type parameter, e any é sua constraint (falaremos sobre isso na próxima seção). A função Primeiro aceita um slice de qualquer tipo e retorna o primeiro elemento. Você chama essa função passando o tipo explicitamente ou deixando o compilador inferir:
// Explícito
numeros := []int{1, 2, 3}
primeiro := Primeiro[int](numeros) // primeiro = 1
// Inferido
palavras := []string{"hello", "world"}
palavra := Primeiro(palavras) // Go infere string automaticamente
Type Parameters em Tipos Personalizados
Você também pode usar type parameters ao definir tipos (structs, interfaces, tipos base):
type Pilha[T any] struct {
elementos []T
}
func (p *Pilha[T]) Empilhar(elemento T) {
p.elementos = append(p.elementos, elemento)
}
func (p *Pilha[T]) Desempilhar() (T, bool) {
var zero T
if len(p.elementos) == 0 {
return zero, false
}
ultimo := p.elementos[len(p.elementos)-1]
p.elementos = p.elementos[:len(p.elementos)-1]
return ultimo, true
}
Você instancia essa pilha para um tipo específico:
pilha := &Pilha[string]{}
pilha.Empilhar("Alice")
pilha.Empilhar("Bob")
nome, ok := pilha.Desempilhar() // nome = "Bob", ok = true
Constraints: Definindo Limites ao Polimorfismo
Constraints definem quais tipos podem ser usados para um type parameter. Sem constraints, você está limitado a operações muito básicas (atribuição, passagem para funções como argumento). Constraints permitem assumir propriedades específicas dos tipos que você está trabalhando.
Constraint any
A constraint any é equivalente a interface{} — permite qualquer tipo. É útil quando você não precisa de operações específicas no type parameter:
func Imprimir[T any](valor T) {
fmt.Println(valor)
}
Imprimir(42)
Imprimir("hello")
Imprimir(3.14)
Constraints Baseadas em Tipos Específicos
Você pode especificar que um type parameter deve ser um dos tipos concretos listados:
func Somar[T int | int64 | float64](a, b T) T {
return a + b
}
resultado1 := Somar(5, 3) // int: 8
resultado2 := Somar(int64(10), 5) // int64: 15
resultado3 := Somar(2.5, 3.7) // float64: 6.2
O operador | na constraint significa "ou", permitindo múltiplos tipos.
Constraints com Interfaces
A abordagem mais poderosa é usar uma interface como constraint. Você define uma interface com os métodos ou características que o type parameter deve ter:
type Comparable interface {
Comparar(outro Comparable) int
}
func Maximo[T Comparable](a, b T) T {
if a.Comparar(b) > 0 {
return a
}
return b
}
Qualquer tipo que implemente a interface Comparable pode ser usado:
type Numero int
func (n Numero) Comparar(outro Comparable) int {
o := outro.(Numero)
if n > o {
return 1
} else if n < o {
return -1
}
return 0
}
max := Maximo(Numero(10), Numero(5)) // Numero(10)
Constraints Pré-definidas (Pacote constraints)
Go fornece algumas constraints prontas no pacote constraints:
import "golang.org/x/exp/constraints"
func MaiorNumero[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
resultado := MaiorNumero(7, 3) // 7
resultado2 := MaiorNumero("zebra", "apple") // "zebra"
A constraint constraints.Ordered funciona com tipos que suportam operadores de comparação (<, >, ==, etc.).
Casos de Uso Reais
Generics resolvem problemas práticos do dia a dia. Vamos explorar cenários onde eles realmente fazem diferença.
Caso 1: Estruturas de Dados Genéricas
Antes de generics, implementar uma fila ou lista reutilizável era complicado. Agora:
type Fila[T any] struct {
items []T
}
func (f *Fila[T]) Enfileirar(item T) {
f.items = append(f.items, item)
}
func (f *Fila[T]) Desenfileirar() (T, bool) {
var zero T
if len(f.items) == 0 {
return zero, false
}
item := f.items[0]
f.items = f.items[1:]
return item, true
}
// Uso
filaDePessoas := &Fila[string]{}
filaDePessoas.Enfileirar("Alice")
filaDePessoas.Enfileirar("Bob")
pessoa, ok := filaDePessoas.Desenfileirar() // "Alice"
Caso 2: Funções Utilitárias em Slices
Operações comuns com slices agora são genéricas:
func Filtrar[T any](items []T, predicado func(T) bool) []T {
resultado := []T{}
for _, item := range items {
if predicado(item) {
resultado = append(resultado, item)
}
}
return resultado
}
numeros := []int{1, 2, 3, 4, 5}
pares := Filtrar(numeros, func(n int) bool { return n%2 == 0 })
// pares = []int{2, 4}
Caso 3: API HTTP com Respostas Genéricas
Um padrão muito comum é uma resposta JSON genérica:
type Resposta[T any] struct {
Sucesso bool `json:"sucesso"`
Dados T `json:"dados"`
Erro string `json:"erro,omitempty"`
}
type Usuario struct {
ID int `json:"id"`
Nome string `json:"nome"`
}
func ObterUsuario(w http.ResponseWriter, r *http.Request) {
usuario := Usuario{ID: 1, Nome: "Alice"}
resposta := Resposta[Usuario]{
Sucesso: true,
Dados: usuario,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resposta)
}
Caso 4: Cache Genérico
Um cache que funciona com qualquer tipo de dado:
type Cache[T any] struct {
dados map[string]T
mu sync.RWMutex
}
func (c *Cache[T]) Set(chave string, valor T) {
c.mu.Lock()
defer c.mu.Unlock()
if c.dados == nil {
c.dados = make(map[string]T)
}
c.dados[chave] = valor
}
func (c *Cache[T]) Get(chave string) (T, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
valor, existe := c.dados[chave]
return valor, existe
}
// Uso
cacheUsuarios := &Cache[Usuario]{}
cacheUsuarios.Set("user:1", Usuario{ID: 1, Nome: "Alice"})
usuario, ok := cacheUsuarios.Get("user:1")
Caso 5: Constraint com Múltiplos Métodos
Às vezes você precisa que um type parameter implemente vários métodos:
type Serializavel interface {
Marshal() ([]byte, error)
Unmarshal([]byte) error
}
func ProcessarDados[T Serializavel](dados T) ([]byte, error) {
return dados.Marshal()
}
Boas Práticas e Limitações
Generics são poderosos, mas exigem moderação. Use generics quando eles reduzirem duplicação de código de verdade. Não use quando tornar o código mais complexo sem benefício claro.
Quando NÃO usar Generics
- Quando uma solução com
interface{}é suficiente e clara - Quando a lógica depende muito do tipo específico (melhor é duplicar código)
- Em tipos com apenas alguns casos específicos
Quando USAR Generics
- Estruturas de dados que armazenam múltiplos tipos (pilhas, filas, árvores)
- Funções utilitárias que operam em slices ou maps de qualquer tipo
- APIs que retornam respostas genéricas
- Quando você vê duplicação de código idêntico com tipos diferentes
Conclusão
Generics em Go trouxe três melhorias fundamentais: reutilização de código sem sacrificar segurança de tipos, eliminação de type assertions desnecessários e clareza na intenção do código (quando bem utilizados). Type parameters e constraints funcionam juntos para permitir polimorfismo seguro. Na prática, você usará generics principalmente em estruturas de dados, funções utilitárias e padrões de API genérica — mas sempre com moderação, preferindo simplicidade quando possível.