Pular para o conteúdo principal

Generics

10 min de leitura•Arquivado emLinguagem de Programação Goem

Aprenda como os generics do Go permitem reuso de código com type safety, usando type parameters e constraints para eliminar duplicação e casts inseguros.

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 → T ou map[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.