Aula 23 - Tutorial Golang - Generics
Generics
A partir da
versão 1.18, o
Go adicionou suporte a
genéricos, também conhecidos como
parâmetros de tipo.
Serve para criar
funções e
tipos genéricos em
Go.
Quando escrevemos funções, geralmente as declaramos para receber parâmetros de algum tipo específico, como
int,
float,
string, etc.
Exemplo:
func PrintInt(v int) {
println(v)
}
Aqui, a função
PrintInt() recebe um único parâmetro
v, do tipo
int, e imprime ele!
Teríamos um problema se quiséssemos chamar essa mesma função com um valor de outro tipo, por exemplo,
float64 ou
string.
Não vai funcionar!
O sistema de tipos de
Go garante que só podemos passar o tipo de valor que a função espera.
Tipo Estrito
Às vezes, o sistema de tipo estrito nos prejudica mais do que nos ajuda.
Por exemplo, se quisermos escrever uma biblioteca de utilitários com funções operando em variáveis de tipos diferentes.
Teríamos que escrever dezenas de funções semelhantes, uma para cada tipo possível, ou usar algum tipo de solução complicada envolvendo interfaces.
Funções Genéricas
Agora não precisamos mais fazer isso, porque podemos escrever
funções genéricas!
Uma
função genérica recebe parâmetros não de algum tipo nomeado especificamente como
int por exemplo, mas sim, algum tipo arbitrário que não precisamos especificar antecipadamente.
O
T é de "
tipo".
Esse espaço reservado
T é chamado
parâmetro de tipo.
Quando chamarmos essa função em nosso programa, estaremos chamando ela com um valor de algum tipo específico, talvez
int, ou um
float64, ou qualquer outra coisa.
Mas, não queremos ter que especificar isso antecipadamente quando estivermos escrevendo a função, porque queremos escrever uma função de propósito geral, que realmente não se importe com o tipo em que vai operar.
Então, vamos usar o tipo genérico
T.
Podemos ler a PrintAnything[T] como:
Para qualquer tipo
T,
PrintAnything[T] recebe um parâmetro do tipo
T.
Essa sintaxe pode parecer confusa no início, principalmente para novatos em
Go.
Temos o nome da função
PrintAnything e a lista usual de parâmetros entre parênteses
(s []T), mas inserimos algo novo no meio, o
[T any].
Parâmetros de tipo
Então, qualquer que seja o tipo específico, ou tipos,
T acaba sendo usado quando esta função é chamada.
O parâmetro
v de
PrintAnything será desse tipo.
Por exemplo, pode ser
int.
Enquanto
v é apenas um parâmetro comum, de algum tipo não especificado,
T é diferente.
T é um novo tipo de parâmetro em
Go, como falado antes, é chamado
parâmetro de
tipo.
Dizemos que
PrintAnything[T] é uma
função parametrizada, ou seja, uma
função genérica de algum tipo
T.
Instanciação
O que é o tipo T especificamente?
Depende do que decidimos passar para a função quando ela é chamada.
Vamos ver como isso funciona!
Suponha que o chamemos com um argumento
int por exemplo.
Damos o nome da função, como de costume, e selecionamos qual
T específico queremos, colocando-o entre colchetes após o nome.
var x int = 5
PrintAnything[int](x)
O que fizemos acima é chamado de instanciar a função.
A função genérica
PrintAnything é como um
tipo de
template e quando a chamamos com algum tipo específico, criamos uma instância específica da função que recebe esse tipo.
Podemos imaginar o compilador vendo esta chamada para
PrintAnything[int](x) e pensando:
"Aha! Agora eu sei o que T é int, então vou compilar uma versão de PrintAnything que recebe um parâmetro int".
E é isso que acontece!
Na verdade, geralmente não precisamos instanciar explicitamente a função fornecendo o nome do tipo entre colchetes.
Onde o compilador pode inferir esse tipo a partir do contexto, podemos deixá-lo de fora, então fica igual a uma chamada de função
não genérica comum.
var x int = 5
PrintAnything(x)
E se tivermos outra chamada para
PrintAnything[T] em outro lugar no programa e desta vez
T for de um tipo diferente, como uma
string?
Beleza agora, sem problemas.
O compilador produzirá outra versão de
PrintAnything, desta vez uma que recebe um argumento de
string.
Para cada instância distinta de uma função genérica em um programa específico, o compilador produzirá uma implementação distinta da função, que usa o tipo concreto necessário como parâmetro.
Estêncil (Stencilling)
Essa abordagem de implementação de genéricos é chamada de
stencilling, que é um nome bastante apropriado.
Você pode imaginar o compilador pintando com spray um monte de versões semelhantes da função, todas usando o mesmo "estêncil", e diferindo apenas no tipo de parâmetro que elas usam.
Poderíamos ter feito a mesma coisa usando o maquinário de geração de código existente em Go, e de fato muitas pessoas fizeram exatamente isso antes da introdução dos genéricos.
Isso cria um código de máquina eficiente, porque não há
indireção ao contrário dos valores de interface.
Ao contrário de
interface que tem uma natureza abstrata, funcionando como um mecanismo que "oculta" detalhes complicados de um objeto, a
indireção refere-se a tornar a localização de um item transparente.
Uma variável é uma
indireção, podemos acessar uma posição de memória, mas, usamos um nome que nos leva a essa posição da memória.
Não precisamos de asserções de tipo, porque cada implementação diferente de
PrintAnything sabe exatamente qual tipo concreto está recebendo.
Este não é um exemplo particularmente convincente de genéricos em ação, porque já era possível escrever
PrintAnything usando um parâmetro de interface vazia.
Podemos simplesmente passá-lo direto para
fmt.Println(), que também aceita
interface{}.
Função Identity()
Vejamos um exemplo um pouco mais interessante, embora ainda bastante artificial.
Suponha que queremos escrever uma função chamada
Identity() que simplesmente retorna qualquer valor que você passar.
Como poderíamos escrever isso?
É aqui que começamos a ir além dos limites das interfaces.
Usando a
interface{}, por exemplo, teríamos que escrever algo como:
func Identity(v interface{}) interface{} {
return v
}
Isso funciona, mas não é realmente satisfatório.
Não temos como dizer ao compilador que o parâmetro da função e seu resultado devem ser do mesmo tipo concreto, seja ele qual for.
Parâmetro de Tipo
Agora sabemos como fazer isso usando um parâmetro de tipo:
func Identity[T any](v T) T {
return v
}
Lembra como ler isso?
Para qualquer tipo
T,
Identity[T] recebe um parâmetro do tipo
T e retorna um resultado do tipo
T.
Instanciando Identidade
Suponha que chamemos esta função em algum lugar do nosso programa com um argumento
string.
fmt.Println(Identity("Hello"))
// Hello
Agora você sabe como isso funciona.
Nos bastidores, o compilador gera uma
versão instanciada de
Identity que recebe um parâmetro de
string e
retorna um resultado em
string.
Esta é apenas uma função
Go simples e comum que poderíamos ter escrito ou gerado mecanicamente.
O ponto é, claro que não precisamos fornecer uma versão separada de
Identity para cada tipo concreto que queremos usar.
Em vez disso, apenas escrevemos uma vez para algum tipo arbitrário
T, e o compilador gerará automaticamente uma versão de
Identity para cada tipo que é realmente usado em nosso programa.
Vamos a um código completo para a gente testar.
Código 01 completo
generics_01.go
package main
import "fmt"
func PrintAnything[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
func PrintInt(v int) {
println(v)
}
func main() {
PrintAnything([]string{"Hello, ", "World"})
PrintAnything([]int{4, 2})
PrintAnything([]float64{3.7, 2.9})
PrintInt(7)
}
Saída:
Hello,
World
4
2
3.7
2.9
7
Código 02
Função Genérica MapKeys
Veja no código a seguir, um exemplo de uma função genérica, o
MapKeys, ele pega um mapa de qualquer tipo e retorna um array com um pedaço (
Slice) só com as chaves.
Esta função tem dois parâmetros de tipo -
K (
chave) e
V(
valor).
O
K tem a restrição comparável, o que significa que podemos comparar valores desse tipo com os operadores
== e
!=.
Isso é necessário para chaves de mapa em Go.
O
V tem a restrição
any, o que significa que não é restrito de forma alguma (
any é um alias para
interface{} ).
O código a seguir recebe um
map chave valor e retorna um
array com as
chaves.
O
r é esse array, onde o tamanho é definido com o
make(), obviamente, com o mesmo tamanho do
map, por isso de
0 até
len(m).
Em seguida, percorre esse
map, pegando só as
chaves e
populando o
array r com as
chaves do
map usando o
append().
Por fim, o
array só com as
chaves do
map é retornado pela função
MapKeys().
func MapKeys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
Tipo Genérico List[T]
Como exemplo de um
tipo genérico,
List é uma lista ligada, com valores de qualquer tipo (
any).
Tanto
head como
tail apontam para um elemento genérico na lista.
Cada elemento da lista terá um ponteiro para o próximo item dessa lista(
next *element[T]) e além desse ponteiro para outro elemento, o elemento guarda também uma informação, isto é, um valor (
val T).
type List[T any] struct {
head, tail *element[T]
}
type element[T any] struct {
next *element[T]
val T
}
Métodos Para Tipos Genéricos
Podemos definir métodos em tipos genéricos, assim como fazemos em tipos regulares, mas, temos que manter os parâmetros de tipo no lugar.
Veja abaixo a implementação do
Push() e do
GetAll().
O
Push() insere os valores na lista e atualiza o ponteiro para o próximo elemento.
O
GetAll() retorna a lista com os elementos inseridos.
OBS. O tipo é
List[T], não
List.
func (lst *List[T]) Push(v T) {
if lst.tail == nil {
lst.head = &element[T]{val: v}
lst.tail = lst.head
} else {
lst.tail.next = &element[T]{val: v}
lst.tail = lst.tail.next
}
}
func (lst *List[T]) GetAll() []T {
var elems []T
for e := lst.head; e != nil; e = e.next {
elems = append(elems, e.val)
}
return elems
}
Ao invocar funções genéricas, em geral podemos confiar na inferência de tipos.
Observe que não precisamos especificar os tipos para
K e
V ao chamar
MapKeys, o compilador os infere automaticamente, embora também possamos especificá-los explicitamente.
func main() {
var m = map[int]string{1: "2", 2: "4", 3: "8"}
// Ao invocar funções genéricas, muitas vezes podemos confiar
// na inferência de tipos. Observe que não precisamos
// especificar os tipos para K e V ao chamar MapKeys
// o compilador os infere automaticamente
fmt.Println("keys m:", MapKeys(m))
// ... embora também possamos especificá-los explicitamente.
_ = MapKeys[int, string](m)
lst.Push(11)
fmt.Println("list:", lst.GetAll())
lst.Push(13)
fmt.Println("list:", lst.GetAll())
lst.Push(29)
fmt.Println("list:", lst.GetAll())
lst.Push(37)
fmt.Println("list:", lst.GetAll())
lst.Push(41)
fmt.Println("list:", lst.GetAll())
}
Abaixo, as imagens mostram a lógica do código da lista ligada
Código 02 completo
generics_02.go
package main
import "fmt"
// Como exemplo de uma função genérica, MapKeys pega um
// mapa de qualquer tipo e retorna uma fatia de suas chaves.
// Esta função tem dois parâmetros de tipo - K (chave) e V(valor).
// O K tem a restrição comparável, o que significa que podemos
// comparar valores desse tipo com os operadores == e !=.
// Isso é necessário para chaves de mapa em Go.
// O V tem a restrição any, o que significa que não é restrito
// de forma alguma ( any é um alias para interface{} ).
func MapKeys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
// Podemos definir métodos em tipos genéricos assim como fazemos em tipos
// regulares, mas temos que manter os parâmetros de tipo no lugar.
type List[T any] struct {
head, tail *element[T]
}
type element[T any] struct {
next *element[T]
val T
}
func (lst *List[T]) Push(v T) {
if lst.tail == nil {
lst.head = &element[T]{val: v}
lst.tail = lst.head
} else {
lst.tail.next = &element[T]{val: v}
lst.tail = lst.tail.next
}
}
func (lst *List[T]) GetAll() []T {
var elems []T
for e := lst.head; e != nil; e = e.next {
elems = append(elems, e.val)
}
return elems
}
func main() {
var m = map[int]string{1: "2", 2: "4", 4: "8"}
// Ao invocar funções genéricas, muitas vezes podemos confiar
// na inferência de tipos. Observe que não precisamos
// especificar os tipos para K e V ao chamar MapKeys
// o compilador os infere automaticamente
fmt.Println("keys m:", MapKeys(m))
// ... embora também possamos especificá-los explicitamente.
_ = MapKeys[int, string](m)
lst := List[int]{}
lst.Push(11)
lst.Push(13)
lst.Push(29)
lst.Push(37)
lst.Push(41)
fmt.Println("list:", lst.GetAll())
}
E pra executar é só entrar na pasta onde tá o arquivo generics.go
go run generics.go
Saída:
keys m: [1 2 3]
list: [11 13 29 37 41]
É isso pessoal, fico por aqui!
Até mais. :)
página do Código Fluente no
Facebook
Sigam o Código Fluente no Instagram e no TikTok.
Esse é o link do código fluente no Pinterest
Meus links de afiliados:
Obrigado e bons estudos. ;)