Go Admin

Dominando Generics em Go: Type Parameters, Constraints e Casos de Uso Reais em Projetos Reais Já leu

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

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.

Referências


Artigos relacionados