O problema antes dos generics
Antes do Go 1.18, escrever uma função que funcionasse para múltiplos tipos significava escolher entre dois caminhos ruins. O primeiro era duplicação: escrever a mesma lógica uma vez para cada tipo necessário:
O segundo era usar interface{} (agora com o alias any) — o que jogava fora toda a type safety e obrigava os chamadores a fazer type assertions que podiam causar panic em runtime.
Os generics resolvem isso. Eles permitem escrever uma única função que funciona para muitos tipos enquanto o compilador verifica a corretude de tipos em cada chamada.
Type parameters
Uma função genérica declara uma lista de type parameters entre colchetes antes da lista de parâmetros regular:
T é o type parameter — um placeholder para um tipo real que será preenchido quando a função for chamada. int | float64 | string é a constraint — ela especifica quais tipos podem substituir T. O corpo da função usa T exatamente como se fosse um tipo concreto.
Para chamar uma função genérica, o compilador precisa saber qual é o T. Você pode fornecê-lo explicitamente:
Mas na prática você quase nunca precisa fazer isso. O Go infere o type argument a partir dos argumentos da chamada.
Type inference
Quando o Go consegue determinar o type argument a partir dos valores que você passa, você não precisa escrevê-lo:
A type inference funciona na grande maioria dos casos. Type arguments explícitos só são necessários quando a inferência é ambígua — o que é raro em funções genéricas bem projetadas.
Constraints
Uma constraint limita quais tipos podem ser substituídos por um type parameter. Constraints são interfaces comuns — toda interface em Go é agora uma constraint válida, e as interfaces ganharam novos poderes especificamente para expressar constraints de tipo. Se você quiser revisar como interfaces funcionam antes de mergulhar na sintaxe de constraints, o artigo sobre interfaces cobre os fundamentos.
any
A constraint any (um alias para interface{}) aceita qualquer tipo. Use-a quando sua função apenas armazena, passa ou retorna o valor — sem precisar realizar operações específicas do tipo:
First funciona para um slice de qualquer tipo de elemento. Ela não compara nem soma elementos — apenas lê, então any é a constraint correta.
comparable
A constraint built-in comparable aceita qualquer tipo que suporte == e !=. Isso inclui todos os tipos numéricos, strings, booleans, pointers, channels, arrays de elementos comparáveis e structs cujos campos são todos comparáveis.
O compilador impõe isso: se você tentar usar == em um valor cujo type parameter tem constraint any, o código não compila. comparable é a constraint correta sempre que sua função compara valores por igualdade.
Constraints de interface
Qualquer interface que define métodos pode servir como constraint. O corpo da função pode então chamar esses métodos em valores do type parameter:
O compilador sabe que todo tipo substituído por T implementa Stringer, então chamar item.String() é válido.
Union constraints
Interfaces podem incluir um type set — tipos concretos separados por |. O type parameter só pode ser substituído pelos tipos listados no set, e o corpo da função pode apenas usar operações suportadas por todos eles:
O operador += é válido aqui porque todos os tipos em Number suportam adição. Se você tentasse usar uma operação que apenas alguns tipos suportam, o compilador rejeitaria.
O prefixo ~
Um elemento de constraint prefixado com ~ corresponde a qualquer tipo cujo underlying type seja aquele tipo:
Sem ~, Celsius não satisfaria uma constraint de float64 porque é um tipo nomeado distinto. O prefixo ~ estende a constraint para cobrir todos os tipos nomeados construídos sobre aquele underlying type — uma ferramenta essencial para escrever constraints que funcionam com tipos definidos pelo usuário.
cmp.Ordered
O pacote cmp da biblioteca padrão (adicionado no Go 1.21) exporta uma constraint Ordered que cobre todos os tipos que suportam os operadores de ordenação (<, <=, >, >=):
cmp.Ordered se expande para todos os tipos inteiros, float e string — com ~ aplicado a cada um, para que tipos definidos pelo usuário também funcionem. Usá-lo poupa você de escrever essa union manualmente.
Múltiplos type parameters
Uma lista de type parameters pode incluir mais de um parâmetro, cada um com sua própria constraint:
K deve ser comparable porque maps exigem chaves comparáveis. V pode ser any porque a função nunca inspeciona os valores — ela apenas coleta as chaves.
Generic types
Generics se aplicam a declarações de tipos, não apenas a funções. É assim que você constrói estruturas de dados container com type safety:
O type argument é especificado ao declarar uma variável do tipo genérico:
Stack[int] e Stack[string] são tipos distintos — o compilador gera código separado para cada um e impõe suas regras de tipos de forma independente.
O zero value de um type parameter
Quando uma função precisa retornar "nada" para um tipo desconhecido — em caso de erro ou quando uma busca falha — você precisa do zero value de T. O padrão é uma declaração de variável simples:
var zero T inicializa zero com o zero value do tipo que T vier a ser: 0 para tipos numéricos, "" para strings, false para booleans, nil para pointers, slices, maps e channels. Essa é a forma canônica de expressar "zero value de um type parameter".
Generics na biblioteca padrão
O Go 1.21 adotou generics em toda a biblioteca padrão. Os pacotes slices e cmp foram adicionados para fornecer utilitários que antes exigiam implementações de sort.Interface ou casts inseguros com interface{}.
O pacote slices cobre as operações mais comuns em slices:
Antes de slices.Sort, ordenar exigia sort.Slice com uma closure baseada em índices. Com generics, o compilador simplesmente verifica que o tipo do elemento suporta ordenação — sem boilerplate.
O pacote cmp fornece utilitários de comparação junto com a constraint Ordered:
Explorar esses pacotes é uma das formas mais eficazes de aprender código genérico idiomático: eles resolvem problemas reais e cotidianos com o mínimo de cerimônia.
Quando usar generics
Generics brilham quando o mesmo algoritmo funciona para múltiplos tipos e a informação de tipo precisa fluir da entrada para a saída. Os indicadores mais claros são:
- Uma assinatura de função no formato
[]T → T,T → Toumap[K]V → []K - Uma estrutura de dados que precisa funcionar com tipos de elemento escolhidos pelo usuário
- A mesma lógica duplicada em múltiplos tipos no seu codebase
Eles não são substitutos para interfaces. Se sua função só precisa chamar métodos em um valor, use uma interface comum — é exatamente para isso que interfaces existem:
A regra prática: se o type parameter aparece tanto na entrada quanto na saída de forma que preserve a identidade do tipo, generics são apropriados. Se o type parameter só entra e desaparece em uma interface, uma interface comum é mais limpa.