blob: 9168fb18710a3435038156fb11cca1b08ad77d9d [file] [log] [blame] [view]
# Proposal: Extended forwards compatibility in Go
Russ Cox \
December 2022
Earlier discussion at https://go.dev/issue/55092.
Proposal at https://go.dev/issue/57001.
## Abstract
Many people believe the `go` line in the `go.mod` file specifies which Go toolchain to use.
This proposal would correct this widely held misunderstanding by making it reality.
At the same time, the proposal would improve forward compatibility by making sure
that old Go toolchains never try to build newer Go programs.
Define the “work module” as the one containing the directory
where the go command is run. We sometimes call this the “main module”,
but I am using “work module” in this document for clarity.
Updating the `go` line in the `go.mod` of the work module,
or the `go.work` file in the current workspace,
would change the minimum Go toolchain used to run go commands.
A new `toolchain` line would provide finer-grained control over Go toolchain selection.
An environment variable `GOTOOLCHAIN` would control this new behavior.
The default, `GOTOOLCHAIN=auto`, would use the information in `go.mod`.
Setting GOTOOLCHAIN to something else would override the `go.mod`.
For example, to test the package in the current directory with Go 1.17.2:
GOTOOLCHAIN=go1.17.2 go test
## Background
The meaning of the current `go` line in the `go.mod` file is underdocumented
and widely misunderstood.
- Some people believe it sets the minimum version of Go that can be used to build the code.
This is not true: any version of Go will try to build the code, but an older one will
add a note after any compile failure pointing out that perhaps a newer version of Go is needed.
- Some people believe it sets the exact version of Go to use. This is also not true.
The installed version of go is always what runs today.
These are reasonable beliefs. They are just not true.
Today, the only purpose of the `go` line is to determine the Go language version
that the compiler uses when compiling a particular source file.
If a module's `go.mod` says `go 1.16`, then the compiler makes sure to
provide the Go 1.16 language semantics when compiling source files
inside that module.
For example, Go 1.13 added `0o777` syntax for octal literals.
If `go.mod` says `go 1.12`, then the compiler rejects code containing `0o777`.
If `go.mod` says `go 1.13`, then the compiler accepts `0o777`.
Of course, a `go.mod` that says `go 1.13` might still only use Go 1.12 features.
To improve compatibility and avoid ecosystem fragmentation, Go 1.12 will still
try to compile code marked `go 1.13`. If it succeeds, the `go` command
assumes everything went well.
If it fails
(for example, because the code says `0o777` and the compiler
does not know what that means), then the `go` command prints
a notice about the `go 1.13` line after the actual failure,
in case what the user needs to know is to update to Go 1.13 or later.
These version failures are often mysterious, since the compiler errors
betray the older Go's complete and utter confusion
at the new program,
which in turn confuse the developers running the build.
Printing the version mismatch note at the end is better than not printing it,
but it's still not a great experience.
We can improve this experience by having the older Go version
download and re-exec a newer Go version
when the go.mod file needs one.
In this hypothetical world, the Go 1.12 `go` command would
see that it is too old and then download and use Go 1.13 for the build instead.
To be clear, Go 1.12 didn't work this way and never will.
But I propose that some future version of Go should.
Automatic downloading and use of the version of the Go toolchain
listed in the `go.mod` file
would match the automatic download and use
of the versions of required modules listed in the `go.mod` file.
It would also give code a simple way to declare that it needs a newer Go toolchain,
for example because it depends on a bug fix issued in that toolchain.
[Cloud Native Buildpacks](https://buildpacks.io/) are an example of the
bad effects of misunderstanding the meaning of the `go` line.
Today they actually _do_ use the line to select the Go toolchain:
if you have a module that says `go 1.12`, whether you are
trying to keep compatibility with the Go 1.12 language
or you just started out using Go 1.12 and have not needed
to update the line to access any new language features,
Cloud Native Buildpacks will always _build_ your code with Go 1.12,
even if much newer releases of Go exist.
Specifically, they will use the latest point release of Go 1.12.
This choice is unfortunate for two reasons.
First, people are using older releases of Go than they realize.
Second, this leads to non-repeatable builds.
Despite our being very careful, it can of course happen
that code that worked with Go 1.12.8 does not work with Go 1.12.9:
perhaps the code depended on the bug being fixed.
With Cloud Native Buildpacks, a deployment that works today
may break tomorrow if Go 1.12.9 has been released in the interim,
because the chosen release of Go changes based on details not controlled by
the `go.mod`.
If we accidentally issued a Go 1.12.9 that broke all Go programs running in containers,
then every Cloud Native Buildpack user with a `go 1.12` line
would have get broken builds on their next redeploy
without ever asking to update to Go 1.12.9.
This is a perfect example of a [low-fidelity build](https://research.swtch.com/vgo-mvs).
The GitHub Action `setup-go` does something similar
with its [`go-version-file` directive](https://github.com/actions/setup-go#getting-go-version-from-the-gomod-file).
It has the same problems that Cloud Native Buildpacks do.
On the other hand, we can also take Cloud Native Buildpacks and the `setup-go` GitHub Action as
evidence that people expect that line to select the Go toolchain,
at least in the work module.
After all, when a module says `require golang.org/x/sys v0.0.1`,
we all understand that means any build of the module uses that version or later.
Why does `go 1.12` _not_ mean that?
I propose that it should.
For more fine-grained control, I also propose a new `toolchain` line in `go.mod`.
One final feature of treating the `go` version this way is that
it would provide a way to fix for loop scoping,
as discussed in [discussion #56010](https://github.com/golang/go/discussions/56010).
If we make that change, older Go toolchains must not assume
that they can compile newer Go code successfully just because
there are no compiler errors. So this proposal is a prerequisite
for any proposal to do the loop change.
See also my [talk on this topic at GopherCon](https://www.youtube.com/watch?v=v24wrd3RwGo).
## Proposal
The proposal has five parts:
- the GOTOOLCHAIN environment and configuration variable,
- a change to the way the `go` line is interpreted in the work module along with a new `toolchain` line,
- changes to `go get` to allow updating the `go` toolchain,
- a special case to allow Go distributions to be downloaded like modules,
- and changing the `go` command startup procedure.
### The GOTOOLCHAIN environment and configuration variable
The GOTOOLCHAIN environment variable,
configurable as usual with `go env -w`,
will control which toolchain of Go runs when you run `go`.
Specifically, a new enough installed Go toolchain
will know to consult GOTOOLCHAIN and potentially download
and re-exec a different toolchain before proceeding.
This will allow invocations like
GOTOOLCHAIN=go1.17.2 go test
to test a package with Go 1.17.2. Similarly, to try a release candidate:
GOTOOLCHAIN=go1.18rc1 go build -o myprog.exe
Setting `GOTOOLCHAIN=local` will mean to use the locally installed Go toolchain,
never downloading a different one; this is the behavior we have today.
Setting `GOTOOLCHAIN=auto` will mean to use the release named in the
in the work module's `go.mod` when it is newer than the locally installed Go toolchain.
The default setting of GOTOOLCHAIN will depend on the Go toolchain.
Standard Go releases will default to `GOTOOLCHAIN=auto`,
delegating control to the `go.mod` file.
This is the behavior essentially all Go user would see as the default.
Development toolchains—what you get by checking out the Go repository
and running `make.bash`—will default to `GOTOOLCHAIN=local`.
This is necessary for developers of Go itself, so that when working on Go
you actually use the copy you're working on and not a different copy of Go.
Once the toolchain is selected, it would still look at the `go` version:
if the `go` version is newer than the toolchain being run,
the toolchain will refuse to build the program:
Go 1.29 would refuse to attempt to build code that declares `go 1.30`.
### The `go` and `toolchain` lines in `go.mod` in the work module
The `go` line in the `go.mod` in the work module selects the Go semantics.
When the locally installed Go toolchain is newer than the `go` line,
it provides the requested older semantics directly, instead of invoking a stale toolchain.
([Proposal #56986](https://go.dev/issue/56986) addresses making the older semantics more accurate.)
But if the `go` line names a newer Go toolchain, then the locally installed
Go toolchain downloads and runs the newer toolchain.
For example, if we are running Go 1.30 and have a `go.mod` that says
go 1.30.1
then Go 1.30 would download and invoke Go 1.30.1 to complete the command.
On the other hand, if the `go.mod` says
go 1.20rc1
then Go 1.30 will provide the Go 1.20rc1 semantics itself instead of running the
Go 1.20 rc1 toolchain.
Developers may want to run a newer toolchain but with older language semantics.
To enable this, the `go.mod` file would also support a new `toolchain` line.
If present, the `toolchain` line would specify the toolchain to use,
and the `go` line would only specify the Go version for language semantics.
For example:
go 1.18
toolchain go1.20rc1
would select the Go 1.18 semantics for this module but use Go 1.20 rc1 to build
(all still assuming `GOTOOLCHAIN=auto`; the environment variable
overrides the `go.mod` file).
In contrast to the older/newer distinction with the `go` line,
the `toolchain` line always applies: if Go 1.30 sees a `go.mod`
that says `toolchain go1.20rc1`, then it downloads Go 1.20 rc1.
The syntax `toolchain local` would be like setting `GOTOOLCHAIN=local`,
indicating to always use the locally installed toolchain.
### Updating the Go toolchain with `go get`
As part of this proposal, the `go get` command would change
to maintain the `go` and `toolchain` lines.
When updating module requirements during `go get`,
the `go` command would determine the minimum toolchain
required by taking the minimum of all the `go` lines in the
modules in the build graph; call that Go 1.M.
Then the `go` command would make sure the work module's `go.mod`
specifies a toolchain of Go 1.M beta 1 or later.
If so, no change is needed and the `go` and `toolchain` lines
are left as they are.
On the other hand, if a change is needed, the `go` command would edit the `toolchain` line
or add a new one, set to the latest Go 1.M patch release Go 1.M.P.
If Go 1.M is no longer supported, the `go` command
would use the minimum supported major version instead.
The command `go get go@1.20.1` would modify the `go` line to say `go 1.20.1`.
If the `toolchain` line is too old, then the update process just described would apply,
except that since the result would be matching `go` and `toolchain` lines,
the `toolchain` line would just be removed instead.
For direct control of the toolchain, `go get toolchain@go1.20.1` would
update the `toolchain` line. If too old a toolchain is specified, the command fails.
(It does not downgrade module dependencies to find a way to use an older toolchain.)
Updates like `go get go@latest` (or just `go get go`), `go get -p go`, and `go get toolchain@latest`
would work too.
### Downloading distributions
We have a mechanism for downloading verified software archives today:
the Go module system, including the checksum database.
This design would reuse that mechanism for Go distributions.
Each Go release would be treated as a set of module versions,
downloaded like any module, and checked against the checksum database
before being used.
In addition to this mapping, the `go` command would need to
set the execute bit on downloaded binaries.
This would be the first time we set the execute bit in the module cache,
at least on file systems with execute bits.
(On Windows, whether a file is executable depends only on its extension.)
The execute bit would only be set for the specific case of downloading
Go release modules, and only for the tool binaries.
A version like `go 1.18beta2` would map into the module download
machinery as `golang.org/release` version `v1.18.0-beta2.windows.amd64`
on a Windows/AMD64 system.
The version list (the `/@v/list` file) for the module would only list supported releases,
for use by the `go` command in toolchain updates.
Older releases would still be available when fetched directly,
just not listed in the default version list.
### Go command startup
At startup, before doing anything else, the `go` command would
find the `GOTOOLCHAIN` environment or configuration variable
and the `go` and `toolchain` lines from the work module's `go.mod` file
(or the workspace's `go.work` file)
and check whether it needs to use a different toolchain.
If not (for example, if `GOTOOLCHAIN=local` or if `GOTOOLCHAIN=auto`
and `go.mod` says `go 1.28` and the `go` command knows it is
already the Go 1.28 distribution), then the `go` command continues executing.
Otherwise, it looks for the requested Go release in the module cache,
downloading and unpacking it if needed,
and then re-execs the `go` command from that release.
### Effect in Dependencies
In a dependency module, the `go` line will continue to have
its “language semantics selection” effect,
as described earlier.
The Go toolchain will refuse to build a dependency that needs
newer Go semantics than the current toolchain.
For example if the work module says `go 1.27`
but a dependency says `go 1.28` and the toolchain
selection ends up using Go 1.27, Go 1.27 will see the
`go 1.28` line and refuse to build.
This should normally not happen:
the `go get` command that added the dependency
would have noticed the `go 1.28` line and
updated the work module's `toolchain` line to at least go1.28.
## Rationale
The rationale for the overall change was discussed in the background section.
People initially believe that every version listed a module's
`go.mod` is the minimum version used in any build of that module.
This is true except for the `go` line.
Systems such as Cloud Native Buildpacks have made the
`go` line select the Go toolchain, confusing matters further.
Making the `go` line specify a minimum toolchain version
better aligns with user expectations.
It would also align better with systems like Cloud Native Buildpacks,
although they should be updated to match the new semantics exactly.
The easiest way to do that would be for them to run a Go toolchain
that implements the new rules and let it do its default toolchain selection.
There is a potential downside for CI systems without local download caches:
they might download the Go release modules over and over again.
Of course, such systems already download ordinary modules over and over again,
but ordinary modules tend to be smaller.
Go 1.20 removes all `pkg/**.a` files from the Go distribution,
which cuts the distribution size by about a factor of three.
We may be able to cut the size further in Go 1.21.
The best solution is for CI systems to run local caching proxies,
which would speed up their ordinary module downloads too.
Of course, given the choice between
(1) having to wait for a CI system (or a Linux distribution, or a cloud provider)
to update the available version of Go and
(2) being able to use any Go version at the cost of slightly slower builds, I'd definitely choose (2).
And CI systems that insist on never downloading could force GOTOOLCHAIN=local in the environment,
and then the build will break if a newer `go` line slips into `go.mod`.
Some people have raised a concern about pressure on the build cache
because builds using different toolchains cannot share object files.
If this turns out to be a problem in practice, we can definitely adjust
the build cache maintenance algorithms. [Issue #29561](https://go.dev/issue/29561) tracks that.
## Compatibility
This proposal does not violate any existing compatibility requirements.
It can improve compatibility, for example by making sure that code written for Go 1.30
is never built with Go 1.29, even if the build appears to succeed.
## Implementation
Overall the implementation is fairly short and straightforward.
Documentation probably outweighs new code.
Russ Cox, Michael Matloob, and Bryan Millls will do the work.
There is no working sketch of the current design at the moment.