← Voltar para o blog

Generics em Go: Constraints, Interfaces e Código Reutilizável

Aprenda generics em Go na prática: type parameters, constraints, interfaces, comparable, ~type, slices, mapas e quando evitar abstração desnecessária.

Generics em Go permitem escrever funções e tipos reutilizáveis sem abrir mão de tipagem estática. Antes deles, muita biblioteca precisava escolher entre duplicar código, aceitar interface{} e perder segurança de tipos, ou gerar código com ferramentas externas. Desde Go 1.18, a linguagem oferece type parameters, constraints e type sets para resolver uma parte importante desse problema.

A parte delicada é que generics em Go não foram desenhados para transformar a linguagem em Java, C++ ou TypeScript. Eles são uma ferramenta pontual. Funcionam muito bem para estruturas de dados, helpers de slices e maps, funções matemáticas, wrappers de cache, pipelines de transformação e bibliotecas que precisam preservar o tipo de entrada na saída. Funcionam mal quando viram abstração prematura para esconder lógica de negócio simples.

Este guia mostra como usar generics em Go de forma prática: funções genéricas, constraints, comparable, interfaces com type sets, o operador ~, cuidados com legibilidade e quando preferir uma interface comum. Se você ainda está revisando fundamentos, leia também interfaces em Go, slices, maps e structs e o roadmap Go 2026.

O problema que generics resolvem

Imagine uma função para verificar se um slice de strings contém um valor:

func ContainsString(items []string, target string) bool {
    for _, item := range items {
        if item == target {
            return true
        }
    }
    return false
}

Agora você precisa do mesmo para []int, []int64, []UserID e []Status. Sem generics, as opções antigas eram duplicar funções ou usar interface{}:

func Contains(items []interface{}, target interface{}) bool {
    for _, item := range items {
        if item == target {
            return true
        }
    }
    return false
}

Essa versão compila, mas é ruim para Go moderno. Ela força conversões, perde informação de tipo, aceita combinações sem sentido e pode empurrar erros para runtime. Com generics, você escreve a intenção uma vez e preserva o tipo:

func Contains[T comparable](items []T, target T) bool {
    for _, item := range items {
        if item == target {
            return true
        }
    }
    return false
}

T é um type parameter. A constraint comparable diz que T precisa ser um tipo que pode ser comparado com == e !=. Agora a mesma função funciona para strings, números, booleans, ponteiros, structs comparáveis e tipos definidos pelo usuário baseados nesses tipos.

Type parameters sem mistério

A sintaxe básica é:

func Nome[T Constraint](valor T) T {
    return valor
}

O bloco [T Constraint] declara que a função aceita um tipo chamado T, desde que ele satisfaça a constraint indicada. Por convenção, T é comum para um tipo genérico simples, mas nomes mais claros ajudam quando há mais de um tipo:

func MapSlice[In any, Out any](items []In, fn func(In) Out) []Out {
    out := make([]Out, 0, len(items))
    for _, item := range items {
        out = append(out, fn(item))
    }
    return out
}

Uso:

ids := []int{1, 2, 3}
labels := MapSlice(ids, func(id int) string {
    return fmt.Sprintf("user-%d", id)
})

Na maioria dos casos, o compilador infere os tipos. Você raramente precisa chamar MapSlice[int, string](...) manualmente. Isso mantém o código Go relativamente limpo.

any não significa “faça qualquer coisa”

any é apenas um alias para interface{}. Ele significa: este type parameter aceita qualquer tipo. Mas aceitar qualquer tipo também limita o que você pode fazer dentro da função.

func First[T any](items []T) (T, bool) {
    var zero T
    if len(items) == 0 {
        return zero, false
    }
    return items[0], true
}

Essa função funciona com any porque não precisa comparar, somar, ordenar ou chamar métodos de T. Ela só devolve um item do slice.

Se você tentar comparar dois valores T any, o compilador bloqueia:

func Equal[T any](a, b T) bool {
    return a == b // erro: T pode não ser comparável
}

Para comparar, use comparable:

func Equal[T comparable](a, b T) bool {
    return a == b
}

Essa regra é uma das melhores partes do design de generics em Go: a constraint documenta exatamente quais operações são permitidas.

Constraints com interfaces

Constraints são interfaces. A diferença é que, em contexto genérico, uma interface pode declarar métodos e também um conjunto de tipos permitidos.

Uma constraint para tipos numéricos inteiros poderia ser:

type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

func Sum[T Integer](items []T) T {
    var total T
    for _, item := range items {
        total += item
    }
    return total
}

O | funciona como união de tipos. Ele diz que T pode ser qualquer um daqueles tipos. O ~ é importante: ele permite tipos definidos pelo usuário cujo tipo subjacente é aquele tipo.

Sem ~, esta função aceitaria int, mas rejeitaria um tipo de domínio como:

type Pontos int

Com ~int, Pontos entra no conjunto permitido. Isso é útil em código de produção, porque times Go costumam criar tipos de domínio para evitar misturar identificadores, valores monetários, contadores e status.

Constraints com métodos

Você também pode exigir comportamento, não apenas tipo subjacente:

type Validatable interface {
    Validate() error
}

func ValidateAll[T Validatable](items []T) error {
    for i, item := range items {
        if err := item.Validate(); err != nil {
            return fmt.Errorf("item %d: %w", i, err)
        }
    }
    return nil
}

Isso parece uma interface tradicional, e é mesmo. A diferença é que ValidateAll preserva o tipo concreto de T se você precisar expandir a função depois.

Atenção: se a função só precisa chamar métodos e não precisa preservar tipo, uma interface comum pode ser mais simples:

func ValidateAll(items []Validatable) error { ... }

A versão genérica faz sentido quando você quer operar sobre []T sem converter para []Validatable, porque slices de tipos concretos não são automaticamente slices de interface em Go.

Generics em tipos: cache, stack e repository helpers

Generics também funcionam em tipos:

type Cache[K comparable, V any] struct {
    items map[K]V
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    return &Cache[K, V]{items: make(map[K]V)}
}

func (c *Cache[K, V]) Set(key K, value V) {
    c.items[key] = value
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    value, ok := c.items[key]
    return value, ok
}

Esse padrão aparece em caches locais, registries, estruturas de dados, filas internas, stacks de validação e wrappers de teste. K comparable é necessário porque chaves de map precisam ser comparáveis. V any é suficiente porque o cache apenas armazena e devolve valores.

Uso:

cache := NewCache[string, User]()
cache.Set("42", User{ID: "42", Name: "Ana"})
user, ok := cache.Get("42")

O compilador impede que você grave um Product em um cache de User. Esse é o tipo de segurança que interface{} não dava.

Quando generics deixam o código pior

O erro mais comum é criar abstração antes de haver repetição real. Go continua valorizando código explícito. Se uma função genérica exige três constraints, cinco type parameters e nomes abstratos demais, talvez a duplicação pequena seja mais clara.

Evite generics quando:

  • a lógica só existe em um lugar;
  • o tipo genérico não preserva informação útil;
  • a constraint fica mais difícil de entender que a função concreta;
  • você está tentando simular herança;
  • o código de negócio fica escondido atrás de helpers genéricos demais;
  • uma interface pequena resolveria melhor.

Use generics quando:

  • o algoritmo é igual para vários tipos;
  • a função precisa devolver o mesmo tipo que recebeu;
  • você quer evitar interface{} e type assertions;
  • a constraint é simples e expressa uma operação real;
  • a API pública de uma biblioteca fica mais segura.

Um bom teste: se a pessoa lendo o código consegue entender a constraint em menos de dez segundos, provavelmente está aceitável. Se precisa estudar uma hierarquia de constraints para entender uma função de negócio, você passou do ponto.

Generics, interfaces e mercado Go no Brasil

Em entrevistas, generics aparecem menos como pergunta isolada e mais como sinal de maturidade. Empresas que usam Go em backend, plataformas internas, fintechs e sistemas distribuídos querem saber se você sabe escolher a ferramenta certa. Saber escrever um helper genérico é útil; saber explicar por que não usou generics também é.

Para portfólio, um bom exercício é criar uma pequena biblioteca interna com MapSlice, FilterSlice, Contains, Set baseado em map e um cache tipado. Depois use esses helpers em uma API real com testes. Isso mostra domínio sem parecer academicismo.

Se você está buscando vaga, compare esse estudo com a página de vagas Go no Brasil e com o guia de perguntas de entrevista Go. Muitas descrições de vaga citam Go, Kubernetes, APIs REST, gRPC e mensageria, mas a entrevista costuma voltar para fundamentos: tipos, interfaces, concorrência, testes e clareza de código. Para ampliar a busca fora de Go puro, o eu.dev.br ajuda a acompanhar vagas tech no Brasil por stack e senioridade.

Checklist prático

Antes de usar generics em Go, revise:

  1. Existe repetição real entre tipos diferentes?
  2. A função precisa preservar o tipo de entrada na saída?
  3. any é suficiente ou você precisa de comparable?
  4. A constraint deveria aceitar tipos definidos pelo usuário com ~?
  5. Uma interface comum seria mais simples?
  6. O nome do type parameter comunica algo útil?
  7. O código continua legível para alguém do time que não escreveu a abstração?
  8. Há testes cobrindo pelo menos dois tipos diferentes?

Generics em Go são melhores quando parecem pequenos. Eles removem duplicação sem mudar o estilo da linguagem. O objetivo não é escrever menos linhas a qualquer custo; é escrever APIs mais seguras, helpers mais claros e bibliotecas que deixam o compilador trabalhar a favor do time.