Introdução: o que são Generics e por que surgiram em Go?
\nPor muito tempo, a linguagem Go (Golang) permaneceu como uma das poucas linguagens modernas sem suporte a programação genérica (generics). Os desenvolvedores precisavam duplicar código para diferentes tipos de dados ou usar interfaces e interface{} vazios, o que levava à perda de segurança de tipos e à necessidade de realizar conversões de tipos em tempo de execução.
Com o lançamento da versão Go 1.18 (fevereiro de 2022), a linguagem finalmente recebeu os tão esperados genéricos. Generics é um mecanismo que permite escrever funções, tipos e estruturas de dados que funcionam com tipos arbitrários, mantendo a tipagem estrita em tempo de compilação.
\nNeste artigo, abordaremos:
\n- \n
- Para que servem os genéricos em Go; \n
- Como declarar funções e tipos genéricos; \n
- O que são constraints e como usá-las; \n
- Exemplos práticos de aplicação. \n
Para que servem os Generics em Go?
\nAntes dos genéricos, um problema típico era: escrever uma função que trabalhasse com slices de diferentes tipos (por exemplo, int, string, float64).
Sem genéricos, era necessário escrever três funções separadas:
\nfunc SumInts(nums []int) int {\n total := 0\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nfunc SumFloats(nums []float64) float64 {\n total := 0.0\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nOu usar interface{} com conversão de tipos — inseguro e inconveniente.
Com genéricos, basta uma única função:
\nfunc Sum[T int | float64](nums []T) T {\n var total T\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nAs vantagens são óbvias:
\n- \n
- Segurança de tipos — erros de tipo são detectados em tempo de compilação; \n
- Redução de duplicação de código — uma função para muitos tipos; \n
- Legibilidade — o código se torna mais conciso e claro. \n
Fundamentos da sintaxe de Generics em Go
\nDeclaração de uma função genérica
\nA sintaxe básica usa colchetes [] para especificar os parâmetros de tipo:
func NomeDaFunção[NomeDoTipo constraint](parâmetros) resultado {\n // corpo da função\n}\n\nExemplo de uma função simples que retorna o valor recebido:
\nfunc Identity[T any](value T) T {\n return value\n}\n\nfunc main() {\n fmt.Println(Identity[int](42)) // 42\n fmt.Println(Identity[string]("Go")) // Go\n}\n\nGo pode inferir automaticamente o tipo, portanto a especificação explícita [int] é opcional:
fmt.Println(Identity(42)) // Go entende que T = int\nfmt.Println(Identity("Go")) // T = string\n\n\nTipos genéricos (structs)
\nOs genéricos funcionam não apenas com funções, mas também com tipos personalizados:
\ntype Stack[T any] struct {\n elements []T\n}\n\nfunc (s *Stack[T]) Push(value T) {\n s.elements = append(s.elements, value)\n}\n\nfunc (s *Stack[T]) Pop() (T, bool) {\n if len(s.elements) == 0 {\n var zero T\n return zero, false\n }\n value := s.elements[len(s.elements)-1]\n s.elements = s.elements[:len(s.elements)-1]\n return value, true\n}\n\nfunc main() {\n intStack := Stack[int]{}\n intStack.Push(10)\n intStack.Push(20)\n val, ok := intStack.Pop()\n fmt.Println(val, ok) // 20 true\n}\n\n\nConstraints: restrições para tipos
\nNos exemplos acima, usamos any — uma constraint embutida que significa "qualquer tipo". No entanto, muitas vezes é necessário impor restrições: por exemplo, que o tipo suporte comparação ou operações aritméticas.