Uma função genérica
Os parâmetros de tipo ficam entre colchetes, antes dos parâmetros comuns. Cada um tem uma restrição que diz quais tipos são permitidos.
T e U são inferidos a partir dos argumentos, então você raramente escreve Map[int, string](...). A inferência funciona a partir dos argumentos da função; ela não consegue inferir um tipo que aparece só no resultado. Para func Zero[T any]() T, você precisa escrever Zero[int]().
Antes do Go 1.18, essa função precisava ser escrita uma vez para cada tipo, ou receber []interface{} e perder a segurança de tipos.
Restrições
Uma restrição (constraint) é uma interface. Ela limita os tipos que podem ser usados e, em troca, diz ao compilador quais operações são permitidas nos valores desse tipo.
| Restrição | Permite | Deixa você usar |
|---|---|---|
any | qualquer tipo | atribuir, passar, guardar |
comparable | tipos que suportam == | ==, !=, chaves de map |
cmp.Ordered (Go 1.21) | inteiros, floats, strings | <, <=, >, >=, além de == |
| uma interface com métodos | tipos com esses métodos | chamar os métodos |
uma união de tipos como ~int | ~float64 | exatamente esses tipos | operadores que todos eles suportam |
Com any no lugar de comparable, v == target não compila: invalid operation: v == target (incomparable types in type set). É a restrição que torna o operador válido.
Essas duas funções já existem: slices.Index e slices.Max. Os pacotes padrão slices, maps e cmp cobrem a maior parte das necessidades genéricas do dia a dia, então confira neles antes de escrever as suas.
Restrições personalizadas com uniões de tipos e ~
Uma restrição pode listar tipos com |. Os operadores permitidos são os que todos os tipos listados suportam.
O til importa. ~int64 significa "qualquer tipo cujo tipo subjacente seja int64", então o tipo nomeado Cents é aceito. Escrito como int64 puro, Sum([]Cents{...}) falha com Cents does not satisfy Number (possibly missing ~ for int64 in Number).
Interfaces que contêm uniões de tipos só podem ser usadas como restrições, nunca como tipos comuns de variável. var n Number não compila.
O pacote golang.org/x/exp/constraints tem as restrições prontas Integer, Float e Signed. Ele fica fora da biblioteca padrão, então o runner daqui não consegue importá-lo; cmp.Ordered é a restrição padrão.
Tipos genéricos
Structs, slices, maps e outros tipos também podem ter parâmetros de tipo. Métodos em um tipo genérico usam os parâmetros do receiver.
Dois detalhes desse exemplo aparecem o tempo todo:
- O valor zero de T.
var zero Té como você devolve "nada" em código genérico. Não existe umT{}ounilque funcione para todo tipo. - Instanciação. Um tipo genérico precisa ser instanciado antes do uso:
Stack[string], e nãoStack. Dentro dos próprios métodos, o receiver se escreveStack[T].
O Go 1.24 trouxe aliases de tipo genéricos: type Set[T comparable] = map[T]struct{} agora compila.
Limites que você vai encontrar
- Sem parâmetros de tipo em métodos.
func (s *Stack[T]) Map[U any](...)não é permitido. Use uma função de nível superior. - Sem especialização. Você não pode escrever uma implementação separada para
T = string. Um type switch sobreany(v)dentro da função é o contorno, e normalmente um sinal de que generics são a ferramenta errada. - Sem restrições de operador por nome. Não dá para dizer "qualquer tipo com um método
+"; você lista os tipos em uma união. - Acesso a campos por meio de uma restrição não funciona. Mesmo que todos os tipos de uma união tenham um campo
ID,v.IDnão é permitido. Use um método na restrição.
Quando não usar generics
Generics servem para tipos contêiner (pilhas, sets, caches, árvores) e para algoritmos sobre slices e maps que fazem a mesma coisa para qualquer tipo de elemento. Eles não servem para tudo.
- Um único tipo concreto. Se a função só é chamada com
[]User, uma versão genérica acrescenta custo de leitura e mais nada. - Comportamento que varia por tipo. É para isso que existem interfaces. Uma função que recebe um
io.Writeré mais simples que uma que recebe[T io.Writer], e funciona igual. - Substituir parâmetros interface.
func Print[T fmt.Stringer](v T)não é melhor quefunc Print(v fmt.Stringer).
A orientação do próprio time do Go: escreva o código para um tipo específico primeiro, e recorra a parâmetros de tipo quando perceber que está escrevendo o mesmo código pela segunda vez, mudando só os tipos.
Erros comuns
- Usar
anyonde é precisocomparableoucmp.Ordered. O compilador rejeita==ou<em um parâmetro de tipoany. - Esquecer o
~. Tipos definidos pelo usuário comotype ID intfalham em uma restriçãointpura. - Devolver
nilpara um parâmetro de tipo. Não é permitido, a menos que a restrição limite T a tipos parecidos com ponteiro. Devolva um valor zero. - Escrever uma função genérica que já existe. Confira
slices,mapsecmpantes.
Perguntas frequentes
Go tem generics?
Sim, desde o Go 1.18 (março de 2022). Funções e tipos podem declarar parâmetros de tipo entre colchetes: func Map[T, U any](s []T, f func(T) U) []U. Cada parâmetro de tipo tem uma restrição, que é uma interface descrevendo o que o tipo precisa suportar.
Qual a diferença entre any e comparable em generics do Go?
any permite qualquer tipo, mas deixa você fazer quase nada com os valores além de atribuir, passar e guardar. comparable só permite tipos que suportam == e !=, que é o que você precisa para chaves de map ou verificações de igualdade. Para < e >, use cmp.Ordered.
O que o til (~) significa em uma restrição do Go?
~T significa "qualquer tipo cujo tipo subjacente seja T". ~int casa com int e também com type Celsius int. Sem o til, int em uma restrição só casa com o próprio int, e tipos definidos pelo usuário seriam rejeitados.
Métodos em Go podem ter parâmetros de tipo?
Não. Um método pode usar os parâmetros de tipo do tipo do seu receiver (func (s *Stack[T]) Push(v T)), mas não pode declarar novos. Escreva uma função genérica de nível superior: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].