← Voltar para o blog

Generics em Go: Guia Prático com Exemplos Reais

Aprenda Generics em Go com exemplos práticos: type parameters, constraints, funções e structs genéricas, repository pattern e quando não usar generics.

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:

  1. Quando interfaces simples resolvem: se você precisa de polimorfismo comportamental (diferentes tipos com métodos diferentes), interfaces tradicionais continuam sendo a ferramenta certa.

  2. 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.

  3. 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.

  4. Quando any é suficiente: se você não precisa de operações específicas do tipo dentro da função, any como 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:

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.