Menu

Generics em Golang: parâmetros de tipo, restrições e exemplos

Generics (Go 1.18+) permitem que uma função ou um tipo funcione com muitos tipos sem perder a segurança de tipos. Veja parâmetros de tipo, as restrições any, comparable e cmp.Ordered, restrições personalizadas com ~, tipos genéricos e quando não usá-los.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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çãoPermiteDeixa você usar
anyqualquer tipoatribuir, passar, guardar
comparabletipos que suportam ====, !=, chaves de map
cmp.Ordered (Go 1.21)inteiros, floats, strings<, <=, >, >=, além de ==
uma interface com métodostipos com esses métodoschamar os métodos
uma união de tipos como ~int | ~float64exatamente esses tiposoperadores 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 um T{} ou nil que funcione para todo tipo.
  • Instanciação. Um tipo genérico precisa ser instanciado antes do uso: Stack[string], e não Stack. Dentro dos próprios métodos, o receiver se escreve Stack[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 sobre any(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.ID nã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 que func 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 any onde é preciso comparable ou cmp.Ordered. O compilador rejeita == ou < em um parâmetro de tipo any.
  • Esquecer o ~. Tipos definidos pelo usuário como type ID int falham em uma restrição int pura.
  • Devolver nil para 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, maps e cmp antes.

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].

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR