Meu nome é Elton Minetto

Novidade do Go 1.21: Toolchains

Na versão 1.21 da linguagem uma novidade importante foi implementada. Segundo a documentação:

A partir do Go 1.21, a distribuição Go consiste em no comando go e um conjunto de ferramentas (toolchain) incluído, que é a biblioteca padrão, bem como o compilador, montador e outras ferramentas. O comando go pode usar seu toolchain padrão, bem como outras versões encontradas no PATH local ou baixadas conforme necessário.

Para validar como funciona essa nova funcionalidade eu fiz uma prova de conceito, que vou descrever neste post.

Na minha máquina pessoal eu tinha a seguinte versão do go:

go version
go version go1.21.0 darwin/arm64

Criei 3 libs, cada uma com uma versão diferente de requisito. Nos comandos abaixo é possível ver o conteúdo de cada go.mod das libs:

cd l1 ; cat go.mod; cd ..
module github.com/eminetto/l1

go 1.19.0

cd l2; cat go.mod; cd ..
module github.com/eminetto/l2

go 1.20.0

cd l3; cat go.mod; cd ..
module github.com/eminetto/l3

go 1.21.1

A partir da versão 1.21 a linha go 1.20.0 do arquivo go.mod, neste exemplo da lib l2, indica qual é a versão mínima da linguagem que é necessária para que ela seja compilada.

A seguir eu criei um projeto de exemplo, que vai importar as três bibliotecas. O diagrama a seguir mostra as dependências:

toolchain

O main.go do proj contém:

package main

import (
	"fmt"

	"github.com/eminetto/l1"
	"github.com/eminetto/l2"
	"github.com/eminetto/l3"
)

func main() {
	fmt.Println(l1.Sum(1, 2))
	fmt.Println(l2.Sum(1, 2))
	fmt.Println(l3.Sum(1, 2))
}

O go.mod do projeto contém:

module github.com/eminetto/proj

go 1.21.0

require (
	github.com/eminetto/l1 v0.0.0-20231012141607-826d3914801f
	github.com/eminetto/l2 v0.0.0-20231012141624-28120aab8596
	github.com/eminetto/l3 v0.0.0-20231012141624-2810bakab896
)

Executei o projeto e funcionou sem erros:

❯ go run main.go
3
3
3

A seguir eu simulei o processo de atualização de uma das libs. Para isso eu atualizei a lib l3 para uma versão mais nova do Go:

cd l3; cat go.mod
module github.com/eminetto/l3

go 1.21.3

Fiz o push para o repositório e criei uma nova versão (v0.0.1).

Segundo a documentação (tradução minha):

O toolchain recusa-se a carregar um módulo ou workspace que declara uma versão mínima de Go maior do que a versão atual do toolchain.

Para validar isso, no projeto eu atualizei a versão da l3:

❯ go get github.com/eminetto/l3@v0.0.1
go: downloading github.com/eminetto/l3 v0.0.1
go: github.com/eminetto/l3@v0.0.1 requires go >= 1.21.3; switching to go1.21.3
go: upgraded go 1.21.0 => 1.21.3
go: upgraded github.com/eminetto/l3 v0.0.0-20231012141629-a747d5b44b93 => v0.0.1

A novidade é o seguinte trecho:

go: github.com/eminetto/l3@v0.0.1 requires go >= 1.21.3; switching to go1.21.3
go: upgraded go 1.21.0 => 1.21.3

O que aconteceu foi que a versão do Go instalado na minha máquina foi automaticamente atualizado para a versão 1.21.3:

go version
go version go1.21.3 darwin/arm64

E o go.mod do projeto também foi atualizado para a versão 1.21.3:

❯ cat go.mod
module github.com/eminetto/proj

go 1.21.3

require (
        github.com/eminetto/l1 v0.0.0-20231012141607-826d3914801f
        github.com/eminetto/l2 v0.0.0-20231012141624-28120aab8596
        github.com/eminetto/l3 v0.0.1
)

E o projeto continua executando sem erros. O comando go foi atualizado para a versão mínima necessária para poder importar TODAS as libs e executar sem erros. Neste caso atualizou para a 1.21.3 porque era a necessária para rodar a l3, já que as demais estão abaixo da versão que eu tinha no momento instalada na minha máquina (1.21.0). O interessante é que ao invés de quebrar o projeto ele foi adaptado para executar com as versões mínimas necessárias.

Outro ponto que observei é que agora existe uma versão de Go para o projeto e outra para o restante do sistema operacional:

cd proj

Developer/post-toolchain/proj
❯ go version
go version go1.21.3 darwin/arm64

Developer/post-toolchain/proj
cd ..

~/Developer/post-toolchain
❯ go version
go version go1.21.0 darwin/arm64

O comando go identifica que para o diretório proj é necessária uma versão específica da linguagem. E todo esse gerenciamento é feito de maneira automática.

Vamos agora pensar em um exemplo um pouco mais complexo. Vamos supor que uma das libs, por exemplo a l2, fosse atualizada para:

module github.com/eminetto/l2

go 1.24rc1

O comando go obtém a lista de toolchains disponíveis e descobre que os lançamentos mais recentes são Go 1.28.3, Go 1.27.9, e o Go 1.29rc2. Nesta situação, o comando go escolherá Go 1.27.9.

Se a l2 exigisse Go 1.28 ou posterior, o comando go escolheria Go 1.28.3, porque Go 1.27.9 é muito antigo. Se a l2 exigisse Go 1.29 ou posterior, seria escolhida o Go 1.29rc2, porque as outras opções são muito antigas.

Ao executar o comando go get github.com/eminetto/l2@v0.0.1 (a versão mais nova do pacote), o arquivo go.mod do projeto seria atualizado e uma nova linha seria adicionada, com o toolchain escolhido:

❯ cat go.mod
module github.com/eminetto/proj

go 1.27.9

toolchain go1.27.9

require (
        github.com/eminetto/l1 v0.0.0-20231012141607-826d3914801f
        github.com/eminetto/l2 v0.0.1
        github.com/eminetto/l3 v0.0.1
)

Mais detalhes sobre as motivações por trás desta importante feature, a nomenclatura das versões, bem como configurações avançadas de comportamento podem ser vistas no documentação oficial da linguagem.

Este é mais um exemplo do cuidado que a equipe da linguagem tem em manter a compatibilidade entre as versões passadas e futuras. Com esta funcionalidade vai ser possível garantir a longevidade de aplicações escritas em Go, o que é muito importante para o futuro da linguagem em ambientes cada vez mais complexos.

Agradecimentos aos amigos Matheus Mina, Tiago Temporin e Eduardo Hitek pela revisão do texto e sugestões de melhorias.