Usando Generics no Go
Sim, eles estão chegando. Confira como serão os tão aguardados generics em Go.
Como todo mundo que trabalha com a linguagem deve saber, o Golang nasceu sem tratamento de tipos genéricos. De fato, não era uma preocupação no momento, afinal, ele nasceu pra ser simples e performático e, infelizmente, esse tipo recurso não ajuda nisso. Porém, o Go evoluiu para além de pequenas aplicações executando processos pesados e hoje é encontrado em praticamente tudo: aplicações web, bibliotecas, CLIs e aquilo ignorado durante sua concepção começou a ser sentido pela sua crescente comunidade.
Afinal, o que são generics?
Se você tem experiência com generics em outra linguagem, talvez esse tópico seja redundante, então não ficarei triste pular para o próximo.
Diferente de linguagens baseadas em script, como JavaScript e Python, o Go é fortemente tipado (assim como Java, C#, entre outras). Isso significa que toda variável deve ser anotada com o seu tipo: inteiro, string e por aí vai.
Pense em uma situação em que você precisa construir uma URL e precisa adicionar parâmetros a ela. Um jeito simples* seria utilizar o método abaixo (* Apenas um exemplo, nunca construa URLs assim 😬).
go1func AddParam(url, name, value string) string {
2 if !strings.Contains(url, "?") {
3 url += "?"
4 } else {
5 url += "&"
6 }
7
8 return fmt.Sprintf("%s%s=%v", url, name, value)
9}Isso funciona bem para valores do tipo string. Mas caso meu parâmetro seja um inteiro, teríamos de escrever outro método:
1func AddIntParam(url, name string, value int) string {
2 if !strings.Contains(url, "?") {
3 url += "?"
4 } else {
5 url += "&"
6 }
7
8 return fmt.Sprintf("%s%s=%v", url, name, value)
9}E isso começa a ficar insustentável à medida que novos parâmetros aparecem.
Uma maneira de evitar essa repetição desnecessária de código, que inclusive funciona praticamente desde sempre no Go é usar o tipo interface{}:
1func AddParam(url, name string, value interface{}) string {
2 // Nada muda...
3}Porém, isso deixa a abertura de algum desenvolvedor tentar fazer isso. E acredite, algum desenvolvedor vai tentar fazer isso:
go 1type user struct {
2 Name string
3 Age int
4}
5
6AddParam("www.google.com", "teste", user{
7 Name: "Alan",
8 Age: 33,
9})
10
11// URL gerada: www.google.com?teste={Alan 33}E o resultado pode acabar não sendo o esperado, então seria necessário adicionar algumas verificações:
go 1func AddParam(url, name string, value interface{}) (string, error) {
2 switch v := value.(type) {
3 case string, int:
4 return addValidParam(url, name, value), nil
5 default:
6 return "", fmt.Errorf("%v is an invalid type", v)
7 }
8}
9
10func addValidParam(url, name string, value interface{}) string {
11 if !strings.Contains(url, "?") {
12 url += "?"
13 } else {
14 url += "&"
15 }
16
17 return fmt.Sprintf("%s%s=%v", url, name, value)
18}E talvez isso comece a ficar complexo novamente.
Fugindo um pouco do mundo de Golang, no Java, por exemplo, usamos os generics para definir uma estrutura de dados em tempo de execução, como, por exemplo, nas listas:
java1List<User> list = new ArrayList<>();
2list.add(new User("Alan", 33));
3list.get(0).SomeMethodFromUser()Usando o Go 1.18
Os generics estão disponíveis a partir da versão 1.18 do Go, que até o momento em que escrevo esse post ainda não está disponível oficialmente, porém não é complicado instalar a versão de testes. Se você utiliza o Goland ou IntelliJ, basta apenas baixar pela própria IDE:

Ou se preferir, pela própria linha de comando:
bash1go install golang.org/dl/go1.18rc1@latest
2go1.18rc1 download
3go1.18rc1 run path/to/file.goCaso esteja utilizado Go Modules, também é preciso configurar o go.mod para usar a versão correta:
1module seu-modulo
2
3go 1.18Generics no Go
Para não quebrar a compatibilidade da linguagem, a declaração escolhida para tipos genéricos foi a seguinte:
go1func NomeDoMetodo[NomeDoTipo Tipo](nomeDoPamametro NomeDoTipo) {
2 //...
3}Confuso? Vamos reescrever nosso AddParam usando generics:
1func AddParam[T any](url, name string, value T) (string, error) {
2 switch v := value.(type) {
3 case string, int:
4 return addValidParam(url, name, value), nil
5 default:
6 return "", fmt.Errorf("%v is an invalid type", v)
7 }
8}Segundo a própria documentação, any é um sinônimo para interface{}:
1// any is an alias for interface{} and is equivalent to interface{} in all ways.
2type any = interface{}Ou seja, nosso método funciona exatamente como funcionava anteriormente, mas não é exatamente para isso que foi criado os generics. Podemos definir qualquer tipo, que inclusive podem ser vários, o que nos permitiria remover a validação do parâmetro de entrada.
go1func AddParam[T string | int](url, name string, value T) string {
2 if !strings.Contains(url, "?") {
3 url += "?"
4 } else {
5 url += "&"
6 }
7
8 return fmt.Sprintf("%s%s=%v", url, name, value)
9}Desta forma, receberíamos um erro em tempo de compilação caso algum parâmetro fuja disso:
go1AddParam("www.google.com", "teste", user{
2 Name: "Alan",
3 Age: 33,
4})
5
6// Saída no console: user does not implement string|intDeclarações de interfaces também receberem esse recurso, o que nos permite fazer coisas do tipo:
go1type ValidParam interface {
2 string | int | float32 // Ou qualquer outro tipo
3}
4
5func AddParam[T ValidParam](url, name string, value T) string {
6 // Nada muda...
7}E não para por aí
A outra grande vantagem dos tipos genéricos e evitar fazer cast excessivo entre tipos. Vamos imaginar uma aplicação que precise executar operações paralelas, como transferências de arquivos de uma lista de URLs, porém, por limitações do disco e da própria banda, queremos definir um número máximo de workers simultâneos.
Entidade Download:
1type (
2 Download struct {
3 url string
4 }
5)
6
7func (d Download) Download(_ context.Context) {
8 log.Printf("starting download from %s\n", d.url)
9
10 // Simulando o tempo de download
11 ms := rand.Intn(50) * 100
12 time.Sleep(time.Millisecond * time.Duration(ms))
13
14 log.Printf("dowloaded from %s successfuly\n", d.url)
15}
16
17func NewDownload(url string) Download {
18 return Download{
19 url: url,
20 }
21}E o nosso worker de download:
go 1type (
2 Worker struct {
3 buffer int
4 }
5)
6
7func New(bufferSize int) *Worker {
8 return &Worker{
9 buffer: bufferSize,
10 }
11}
12
13func (w Worker) Work(ctx context.Context, items []Download) {
14 var wg sync.WaitGroup
15 wg.Add(len(items))
16
17 jobs := make(chan Download)
18
19 // Cria um número limitado de workers
20 for i := 1; i <= w.buffer; i++ {
21 go func() {
22 for j := range jobs {
23 // Realiza o download
24 download.Download(ctx)
25 wg.Done()
26 }
27 }()
28 }
29
30 for _, item := range items {
31 // Envia um item através de um canal.
32 // O primeiro worker que receber processa.
33 // Se todos estiverem ocupados, a execução fica presa
34 // até algum worker estar disponível.
35 jobs <- item
36 }
37
38 // Fecha o canal e espera até todos workers ocupados finalizarem.
39 close(jobs)
40 wg.Wait()
41}Funcional e bonito. Porém, isso limita o worker a somente lidar com Downloads:
1func main() {
2 n := 20
3 downloads := make([]Download, n)
4
5 for i := 0; i < n; i++ {
6 downloads[i] = NewDownload(fmt.Sprintf("https://some-item.com?id=%d", i+1))
7 }
8
9 w := New(5)
10 w.Work(context.Background(), downloads)
11}Com alguns ajustes, podemos tornar esse worker mais genérico:
go 1package worker
2
3import (
4 "context"
5 "sync"
6)
7
8type (
9 Fn func(context.Context, interface{})
10
11 Worker struct {
12 fn Fn
13 buffer int
14 }
15)
16
17func New(workerFn Fn, bufferSize int) *Worker {
18 return &Worker{
19 fn: workerFn,
20 buffer: bufferSize,
21 }
22}
23
24func (w Worker) Work(ctx context.Context, items []interface{}) {
25 var wg sync.WaitGroup
26 wg.Add(len(items))
27
28 jobs := make(chan interface{})
29
30 for i := 1; i <= w.buffer; i++ {
31 go func() {
32 for j := range jobs {
33 w.fn(ctx, j)
34 wg.Done()
35 }
36 }()
37 }
38
39 for _, item := range items {
40 jobs <- item
41 }
42
43 close(jobs)
44 wg.Wait()
45}E, fazendo os casts necessários, temos:
go 1func main() {
2 n := 20
3 downloads := make([]interface{}, n)
4
5 for i := 0; i < n; i++ {
6 downloads[i] = NewDownload(fmt.Sprintf("https://some-item.com?id=%d", i+1))
7 }
8
9 downloader := func(ctx context.Context, download interface{}) {
10 download.(Download).Download(ctx)
11 }
12
13 w := worker.New(downloader, 5)
14 w.Work(context.Background(), downloads)
15}Funcional, porém nada bonito. Usando generics, podemos encapsular toda essa verbosidade dentro do worker:
go 1type (
2 Fn[T any] func(context.Context, T)
3
4 Worker[T any] struct {
5 fn Fn[T]
6 buffer int
7 }
8)
9
10func New[T any](workerFn Fn[T], bufferSize int) *Worker[T] {
11 return &Worker[T]{
12 fn: workerFn,
13 buffer: bufferSize,
14 }
15}
16
17func (w Worker[T]) Work(ctx context.Context, items []T) {
18 var wg sync.WaitGroup
19 wg.Add(len(items))
20
21 jobs := make(chan T)
22
23 for i := 1; i <= w.buffer; i++ {
24 go func() {
25 for j := range jobs {
26 w.fn(ctx, j)
27 wg.Done()
28 }
29 }()
30 }
31
32 for _, item := range items {
33 jobs <- item
34 }
35
36 close(jobs)
37 wg.Wait()
38}E nosso método main() volta a ser (quase) o que era:
1func main() {
2 n := 20
3 downloads := make([]Download, n)
4
5 for i := 0; i < n; i++ {
6 downloads[i] = NewDownload(fmt.Sprintf("https://some-item.com?id=%d", i+1))
7 }
8
9 downloader := func(ctx context.Context, download Download) {
10 download.Download(ctx)
11 }
12
13 w := worker.New(downloader, 5)
14 w.Work(context.Background(), downloads)
15}Podemos inclusive também escrever listas (não confundir com arrays e slices):
go 1type (
2 List[E any] []E
3)
4
5func NewList[E any]() *List[E] {
6 return &List[E]{}
7}
8
9func (l *List[E]) Add(element E) {
10 *l = append(*l, element)
11}
12
13func (l *List[E]) Get(index int) *E {
14 if index > len(*l) {
15 return nil
16 }
17
18 return &(*l)[index]
19}E até adicionar filtros (adeus for _, item := range items?):
1func (l *List[E]) Find(filter func(*E) bool) *E {
2 for index := range *l {
3 if filter(&(*l)[index]) {
4 return &(*l)[index]
5 }
6 }
7
8 return nil
9}
10
11func main() {
12 type user struct {
13 Name string
14 Age int
15 }
16
17 // Notem que aqui precisamos definir o tipo na inicialização
18 // da struct, usando a anotação [user], pois, o compilador
19 // não teria como inferir esse valor em tempo de execução.
20 list := NewList[user]()
21 list.Add(user{
22 Name: "Alan",
23 Age: 33,
24 })
25
26 im33 := list.Find(func(u *user) bool {
27 return u.Age == 33
28 })
29
30 fmt.Println(im33)
31 // Saída no console: &{Alan 33}
32}Então é isso, galera. Os generics são apenas um dos diversos recursos esperados na versão 1.18 da linguagem que está prevista para sair ainda esse mês, mas eu chutaria só depois do carnaval 😛.
↑