Generics foram introduzidos no Go 1.18 e representam uma das maiores mudanças na linguagem desde sua criação. Com eles, você pode escrever funções e tipos reutilizáveis sem sacrificar a segurança de tipos que Go oferece. Neste guia, vamos explorar generics na prática com exemplos que você pode usar no dia a dia.
O que São Generics?
Antes de generics, quando precisávamos de uma função que operasse sobre diferentes tipos, tínhamos duas opções ruins: duplicar código para cada tipo ou usar interface{} (agora any) e perder a segurança de tipos em tempo de compilação.
Generics resolvem isso permitindo que você parametrize funções e tipos com type parameters — parâmetros que representam tipos, não valores.
// Antes de generics: uma função para cada tipo
func SomaInt(a, b int) int { return a + b }
func SomaFloat(a, b float64) float64 { return a + b }
// Com generics: uma única função genérica
func Soma[T int | float64](a, b T) T {
return a + b
}
func main() {
fmt.Println(Soma(3, 5)) // 8
fmt.Println(Soma(3.14, 2.71)) // 5.85
}
O [T int | float64] é a lista de type parameters. T é o nome do type parameter e int | float64 é a constraint — os tipos que T pode assumir.
Constraints: Definindo os Limites
Constraints definem quais tipos um type parameter pode aceitar. Go oferece constraints pré-definidas e você pode criar as suas.
Constraints Pré-Definidas
import "golang.org/x/exp/constraints"
// any — aceita qualquer tipo
func Imprimir[T any](valor T) {
fmt.Println(valor)
}
// comparable — tipos que suportam == e !=
func Contem[T comparable](slice []T, alvo T) bool {
for _, v := range slice {
if v == alvo {
return true
}
}
return false
}
func main() {
nomes := []string{"Ana", "Bruno", "Carlos"}
fmt.Println(Contem(nomes, "Bruno")) // true
fmt.Println(Contem(nomes, "Diana")) // false
}
Constraints Personalizadas
Você pode criar suas próprias constraints usando interfaces:
// Constraint para tipos numéricos
type Numero interface {
int | int8 | int16 | int32 | int64 |
float32 | float64
}
// Constraint com o operador ~ para aceitar tipos derivados
type NumeroOrdenavel interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64 | ~string
}
func Max[T NumeroOrdenavel](a, b T) T {
if a > b {
return a
}
return b
}
O operador ~ é importante: ~int significa “qualquer tipo cujo tipo subjacente seja int”. Sem o ~, um tipo como type Idade int não seria aceito.
Funções Genéricas na Prática
Vamos ver exemplos reais de funções genéricas que você usaria em projetos do dia a dia.
Map, Filter e Reduce
Operações funcionais sobre slices são um caso de uso clássico para generics:
// Map transforma cada elemento de um slice
func Map[T any, U any](slice []T, fn func(T) U) []U {
resultado := make([]U, len(slice))
for i, v := range slice {
resultado[i] = fn(v)
}
return resultado
}
// Filter retorna elementos que satisfazem o predicado
func Filter[T any](slice []T, fn func(T) bool) []T {
var resultado []T
for _, v := range slice {
if fn(v) {
resultado = append(resultado, v)
}
}
return resultado
}
// Reduce acumula valores em um único resultado
func Reduce[T any, U any](slice []T, inicial U, fn func(U, T) U) U {
acc := inicial
for _, v := range slice {
acc = fn(acc, v)
}
return acc
}
func main() {
numeros := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// Dobrar cada número
dobrados := Map(numeros, func(n int) int { return n * 2 })
// Filtrar apenas pares
pares := Filter(numeros, func(n int) bool { return n%2 == 0 })
// Somar todos
soma := Reduce(numeros, 0, func(acc, n int) int { return acc + n })
fmt.Println(dobrados) // [2 4 6 8 10 12 14 16 18 20]
fmt.Println(pares) // [2 4 6 8 10]
fmt.Println(soma) // 55
}
Utilitários para Slices
// Chaves retorna as chaves de um map
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
}
// Inverter troca chaves e valores de um map
func Inverter[K comparable, V comparable](m map[K]V) map[V]K {
invertido := make(map[V]K, len(m))
for k, v := range m {
invertido[v] = k
}
return invertido
}
Tipos Genéricos: Structs e Métodos
Generics não se limitam a funções — você pode criar structs genéricas também.
Pilha (Stack) Genérica
type Pilha[T any] struct {
elementos []T
}
func (p *Pilha[T]) Push(valor T) {
p.elementos = append(p.elementos, valor)
}
func (p *Pilha[T]) Pop() (T, bool) {
if len(p.elementos) == 0 {
var zero T
return zero, false
}
ultimo := p.elementos[len(p.elementos)-1]
p.elementos = p.elementos[:len(p.elementos)-1]
return ultimo, true
}
func (p *Pilha[T]) Tamanho() int {
return len(p.elementos)
}
func main() {
// Pilha de strings — type-safe
pilha := &Pilha[string]{}
pilha.Push("primeiro")
pilha.Push("segundo")
valor, ok := pilha.Pop()
fmt.Println(valor, ok) // "segundo" true
}
Repository Pattern Genérico
Um caso de uso poderoso para projetos reais é o repository pattern genérico, que elimina a necessidade de criar um repositório por entidade:
type Entidade interface {
GetID() string
}
type Repository[T Entidade] struct {
db *sql.DB
tabela string
}
func NovoRepository[T Entidade](db *sql.DB, tabela string) *Repository[T] {
return &Repository[T]{db: db, tabela: tabela}
}
func (r *Repository[T]) BuscarPorID(ctx context.Context, id string) (T, error) {
var entidade T
query := fmt.Sprintf("SELECT * FROM %s WHERE id = $1", r.tabela)
row := r.db.QueryRowContext(ctx, query, id)
err := row.Scan(&entidade)
return entidade, err
}
func (r *Repository[T]) Listar(ctx context.Context) ([]T, error) {
query := fmt.Sprintf("SELECT * FROM %s", r.tabela)
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var resultados []T
for rows.Next() {
var entidade T
if err := rows.Scan(&entidade); err != nil {
return nil, err
}
resultados = append(resultados, entidade)
}
return resultados, rows.Err()
}
Esse padrão é especialmente útil em combinação com PostgreSQL e clean architecture.
Considerações de Performance
Generics em Go usam uma estratégia de compilação híbrida:
- GC Shape Stenciling: Go gera uma versão da função genérica para cada “shape” de tipo (ponteiro vs. valor). Tipos de ponteiro compartilham a mesma implementação.
- Dictionaries: informações de tipo são passadas em runtime via dicionários internos quando necessário.
Na prática, o overhead é mínimo para a maioria dos casos. Benchmarks mostram que funções genéricas são comparáveis em performance a versões concretas na grande maioria dos cenários.
Quando NÃO Usar Generics
Generics são poderosos, mas nem sempre são a melhor escolha:
Quando interfaces simples resolvem: se você precisa de polimorfismo comportamental (diferentes tipos com métodos diferentes), interfaces tradicionais continuam sendo a ferramenta certa.
Quando o código fica menos legível: se a versão genérica é mais difícil de entender do que versões concretas, prefira a simplicidade.
Quando há apenas um tipo: não crie uma função genérica para um tipo só “por precaução”. Go valoriza código pragmático.
Quando
anyé suficiente: se você não precisa de operações específicas do tipo dentro da função,anycomo parâmetro regular pode ser mais simples.
A regra é: use generics quando perceber duplicação real de lógica entre tipos diferentes. Não use apenas porque é possível.
Próximos Passos
Agora que você entende generics na prática, aprofunde seu conhecimento:
- Interfaces em Go — a base das constraints
- Go para Iniciantes — se está começando com a linguagem
- Testes em Go — como testar funções genéricas
- Go 1.18 — a release que introduziu generics
- Se você trabalha com outras linguagens que usam generics, veja como funciona em Rust ou Kotlin
Generics são uma ferramenta que, quando usada nos cenários certos, deixa seu código Go mais limpo, seguro e fácil de manter.