The version 1.21 of the language implemented an essential new feature. According to the documentation:
Starting in Go 1.21, the Go distribution consists of a go command and a bundled Go toolchain, which is the standard library as well as the compiler, assembler, and other tools. The go command can use its bundled Go toolchain as well as other versions that it finds in the local PATH or downloads as needed.
I did a proof of concept to validate how this new functionality works, which I will describe in this post.
On my machine, I had the following version of go
:
go version
go version go1.21.0 darwin/arm64
I created three libs, each with a different version of go as a requirement. In the commands below, you can see the content of each go.mod
of the 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
Starting with version 1.21, the go 1.20.0
line of the go.mod
file, in this example of the l2
lib, indicates the minimum language version required we need to use to compile it.
Next, I created a project to import the three libraries. The following diagram shows the dependencies:
The main.go
of the proj
contains:
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))
}
The project's go.mod
contains:
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
)
I ran the project, and it worked without errors:
❯ go run main.go
3
3
3
Next, I simulated the process of updating one of the libs.
To do this, I updated the l3
lib to a newer version of Go:
❯ cd l3; cat go.mod
module github.com/eminetto/l3
go 1.21.3
I pushed it to the repository and created a new version (v0.0.1
).
According to the documentation:
The Go toolchain refuses to load a module or workspace that declares a minimum required Go version greater than the toolchain’s own version.
To validate this, in the project, I updated the version of 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
What's new is the following excerpt:
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
What happened was that the go
command updated the version of Go installed on my machine to 1.21.3
:
❯ go version
go version go1.21.3 darwin/arm64
And the project's go.mod
has also been updated to version 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
)
And the project continues to run without errors. My machine now has the minimum version necessary to import ALL libs and run without errors. In this case, it was updated to 1.21.3
because it was necessary to run l3
since the others are below the current version I had installed on my machine (1.21.0
).
Another fact I noticed is that there is now one version of Go for the project and another for the rest of the operating system:
❯ 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
The go
command identifies that the proj
directory requires a specific language version. And all this management is done automatically.
Let's now think about a slightly more complex example. Let's suppose that one of the libs, for example, l2
, was updated to:
module github.com/eminetto/l2
go 1.24rc1
The go
command gets the list of available toolchains
and finds that the most recent releases are Go 1.28.3
, Go 1.27.9
, and Go 1.29rc2
. The go
command will choose Go 1.27.9
in this situation.
If l2
required Go 1.28
or later, the go
command would choose Go 1.28.3
because Go 1.27.9
is too old. If l2
required Go 1.29
or later, Go 1.29rc2
would be chosen because the other options are too old.
When executing the command go get github.com/eminetto/l2@v0.0.1
(the newest version of the package), it will update the project's go.mod
file, and a new line will be added with the chosen toolchain
:
❯ 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
)
You can find more details about the motivations behind this feature, the version naming, and advanced behavior settings in the language's official documentation.
This feature is another example of the language team's care to maintain compatibility between past and future versions. With this functionality, it will be possible to guarantee the longevity of applications written in Go, which is very important for the future of the language in increasingly complex environments.
Thanks to friends Matheus Mina, Tiago Temporin, and Eduardo Hitek for reviewing the text and suggesting improvements.
Originally published at https://eltonminetto.dev on October 18, 2023