Aula 23 – Tutorial Golang – Generics

Aula 23 – Tutorial Golang – Generics

Tutorial Golang - Generics

Tutorial Golang – Generics

Pacote Programador Fullstack

Pacote Programador Fullstack

Página principal do blog

Todas as aulas desse curso

Aula 22                        Aula 24

Se gostarem do conteúdo dêem um joinha 👍 na página do Código Fluente no
Facebook.

Sigam o Código Fluente no Instagram e no TikTok.

Código Fluente no Pinterest.

Meus links de afiliados:

Hostinger

Digital Ocean

One.com

Melhore seu NETWORKING

https://digitalinnovation.one/

Participe de comunidades de desenvolvedores:

Fiquem a vontade para me adicionar ao linkedin.

E também para me seguir no https://github.com/toticavalcanti.

Código final da aula:

https://github.com/toticavalcanti

Canais do Youtube

Toti:

Toti

Backing Track / Play-Along

Código Fluente

Putz!

PIX para doações

PIX Nubank

PIX Nubank

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

Lista ligada imagem 01

Lista ligada imagem 01

Lista ligada imagem 01

Lista ligada imagem 01

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 principal do blog

Todas as aulas desse curso

Aula 22                        Aula 24

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:

Hostinger

Digital Ocean

One.com

Obrigado e bons estudos. 😉

 

 

About The Author
-

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>