Testando o Generics do Go

Elton Minetto - Mar 11 '22 - - Dev Community

Finalmente está (quase) entre nós! Depois de anos ouvindo aquela piadinha "e o Generics?" esta aguardada funcionalidade vai estar disponível na versão 1.18 da linguagem, prevista para lançamento em Março de 2022.

e o Generics?

Neste post eu vou fazer um exemplo usando Generics e um pequeno benchmark para conferir se existem diferenças de performance entre uma função "normal" e outra usando esta nova funcionalidade.

Para isso eu me inspirei na biblioteca lo, uma das primeiras que usa Generics e que ganhou destaque recentemente por implementar várias funcionalidades úiteis para slices e maps.

O primeiro passo foi instalar o Go 1.18, que no momento da escrita deste post encontra-se na versão Release Canditate 1. Para isso eu segui essa documentação e executei os comandos:

go install golang.org/dl/go1.18rc1@latest
go1.18rc1 download
Enter fullscreen mode Exit fullscreen mode

Com isso foi criado o diretório sdk na home do meu usuário no Mac. Vamos usar esse diretório para configurar a IDE, para que ela reconheça a nova versão da linguagem. Eu estou usando o Goland da Jetbrains, então minha configuração ficou desta forma:

generics_goland

Além de criar o diretório sdk os comandos acima criaram o binário go1.18rc1 no diretório go/bin da home do meu usuário no Mac. É esse binário que vamos usar para rodar os testes:

eminetto@MacBook-Pro-da-Trybe ~/D/post-generics [1]> go1.18rc1 version
go version go1.18rc1 darwin/arm64
Enter fullscreen mode Exit fullscreen mode

O próximo passo foi criar um diretório e um main.go:

mkdir post-generics
cd post-generics
go1.18rc1 mod init github.com/eminetto/post-generics
touch main.go
Enter fullscreen mode Exit fullscreen mode

No main.go eu escrevi o seguinte código:

package main

import (
    "fmt"
)

func main() {
    s := []string{"Samuel", "Marc", "Samuel"}
    names := Uniq(s)
    fmt.Println(names)
    names = UniqGenerics(s)
    fmt.Println(names)
    i := []int{1, 20, 20, 10, 1}
    ids := UniqGenerics(i)
    fmt.Println(ids)
}

//from https://github.com/samber/lo/blob/master/slice.go
func UniqGenerics[T comparable](collection []T) []T {
    result := make([]T, 0, len(collection))
    seen := make(map[T]struct{}, len(collection))

    for _, item := range collection {
        if _, ok := seen[item]; ok {
            continue
        }

        seen[item] = struct{}{}
        result = append(result, item)
    }

    return result
}

func Uniq(collection []string) []string {
    result := make([]string, 0, len(collection))
    seen := make(map[string]struct{}, len(collection))

    for _, item := range collection {
        if _, ok := seen[item]; ok {
            continue
        }
        seen[item] = struct{}{}
        result = append(result, item)
    }

    return result
}


Enter fullscreen mode Exit fullscreen mode

Na função main é possível ver a principal vantagem de Generics, pois usamos a mesma função para remover as entradas duplicadas em slices de strings e de inteiros, sem a necessidade de alteração de código.

Ao executar o código podemos ver o resultado:

eminetto@MacBook-Pro-da-Trybe ~/D/post-generics> go1.18rc1 run main.go
[Samuel Marc]
[Samuel Marc]
[1 20 10]
Enter fullscreen mode Exit fullscreen mode

Mas e quanto a performance? Estamos perdendo algo ao adicionar essa nova funcionalidade? Para tentar responder isso eu fiz um pequeno benchmark. O primeiro passo foi instalar o pacote faker, para gerar mais dados para o benchmark:

go1.18rc1 get -u github.com/bxcodec/faker/v3
Enter fullscreen mode Exit fullscreen mode

E o código do main_test.go ficou desta forma:

package main

import (
    "github.com/bxcodec/faker/v3"
    "testing"
)

var names []string

func BenchmarkMain(m *testing.B) {
    for i := 0; i < 1000; i++ {
        names = append(names, faker.FirstName())
    }
}

func BenchmarkUniq(b *testing.B) {
    _ = Uniq(names)
}

func BenchmarkGenericsUniq(b *testing.B) {
    _ = UniqGenerics(names)
}
Enter fullscreen mode Exit fullscreen mode

Executando o benchmark foi possível ver o resultado:

eminetto@MacBook-Pro-da-Trybe ~/D/post-generics> go1.18rc1 test -bench=. -benchtime=100x
goos: darwin
goarch: arm64
pkg: github.com/eminetto/post-generics
BenchmarkMain-8                      100               482.1 ns/op
BenchmarkUniq-8                      100              1225 ns/op
BenchmarkGenericsUniq-8              100              1142 ns/op
PASS
ok      github.com/eminetto/post-generics       0.210s
Enter fullscreen mode Exit fullscreen mode

Eu executei várias vezes o benchmark e na maioria a versão feita com Generics foi mais performática, apesar da diferença não ter sido tão grande.

Observações

Este post não é um estudo avançado, com benchmarks cientificamente comprovados, é apenas um teste básico. Então mais fontes devem ser consultadas antes de tomarmos uma decisão final, mas a primeira impressão é que estamos ganhando uma feature importante sem perda perceptível de performance.

Eu acredito que vou esperar a versão final desta funcionalidade estar mais madura, provavelmente depois da 1.18.x, para colocá-la em produção, mas vejo uma grande evolução nas aplicações Go nos próximos meses. A empolgação está começando a aumentar :)

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .