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:
- Existe repetição real entre tipos diferentes?
- A função precisa preservar o tipo de entrada na saída?
anyé suficiente ou você precisa decomparable?- A constraint deveria aceitar tipos definidos pelo usuário com
~? - Uma interface comum seria mais simples?
- O nome do type parameter comunica algo útil?
- O código continua legível para alguém do time que não escreveu a abstração?
- 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.