blob: 33549a0828c47beca1d49024954fc8aec937eed3 [file] [log] [blame] [view]
---
title: Backward Compatibility, Go 1.21, and Go 2
date: 2023-08-14T12:00:00Z
by:
- Russ Cox
summary: Go 1.21 expands Go's commitment to backward compatibility, so that every new Go toolchain is the best possible implementation of older toolchain semantics as well.
---
Go 1.21 includes new features to improve compatibility.
Before you stop reading, I know that sounds boring.
But boring can be good.
Back in the early days of Go 1,
Go was exciting and full of surprises.
Each week we cut a new snapshot release
and everyone got to roll the dice
to see what we’d changed
and how their programs would break.
We released Go 1 and its compatibility promise
to remove the excitement,
so that new releases of Go would be boring.
Boring is good.
Boring is stable.
Boring means being able to focus on your work,
not on what’s different about Go.
This post is about the important work we shipped in Go 1.21
to keep Go boring.
## Go 1 Compatibility {#go1}
We’ve been focused on compatibility for over a decade.
For Go 1, back in 2012, we published a document titled
“[Go 1 and the Future of Go Programs](/doc/go1compat)”
that lays out a very clear intention:
> It is intended that programs written to the Go 1 specification
> will continue to compile and run correctly, unchanged,
> over the lifetime of that specification. ...
> Go programs that work today should continue to work
> even as future releases of Go 1 arise.
There are a few qualifications to that.
First, compatibility means source compatibility.
When you update to a new version of Go,
you do have to recompile your code.
Second, we can add new APIs,
but not in a way that breaks existing code.
The end of the document warns,
“[It] is impossible to guarantee that no future change will break any program.”
Then it lays out a number of reasons why programs might still break.
For example, it makes sense that if your program depends on a buggy behavior
and we fix the bug, your program will break.
But we try very hard to break as little as possible and keep Go boring.
There are two main approaches we’ve used so far: API checking and testing.
## API Checking {#api}
Perhaps the clearest fact about compatibility
is that we can’t take away API, or else programs using it will break.
For example, here’s a program someone has written
that we can’t break:
package main
import "os"
func main() {
os.Stdout.WriteString("hello, world\n")
}
We can’t remove the package `os`;
we can’t remove the global variable `os.Stdout`, which is an `*os.File`;
and we also can’t remove the `os.File` method `WriteString`.
It should be clear that removing any of those would
break this program.
It’s perhaps less clear that we can’t change the type of `os.Stdout` at all.
Suppose we want to make it an interface with the same methods.
The program we just saw wouldn’t break, but this one would:
package main
import "os"
func main() {
greet(os.Stdout)
}
func greet(f *os.File) {
f.WriteString(“hello, world\n”)
}
This program passes `os.Stdout` to a function named `greet`
that requires an argument of type `*os.File`.
So changing `os.Stdout` to an interface will break this program.
To help us as we develop Go, we use a tool that maintains
a list of each package’s exported API
in files separate from the actual packages:
% cat go/api/go1.21.txt
pkg bytes, func ContainsFunc([]uint8, func(int32) bool) bool #54386
pkg bytes, method (*Buffer) AvailableBuffer() []uint8 #53685
pkg bytes, method (*Buffer) Available() int #53685
pkg cmp, func Compare[$0 Ordered]($0, $0) int #59488
pkg cmp, func Less[$0 Ordered]($0, $0) bool #59488
pkg cmp, type Ordered interface {} #59488
pkg context, func AfterFunc(Context, func()) func() bool #57928
pkg context, func WithDeadlineCause(Context, time.Time, error) (Context, CancelFunc) #56661
pkg context, func WithoutCancel(Context) Context #40221
pkg context, func WithTimeoutCause(Context, time.Duration, error) (Context, CancelFunc) #56661
One of our standard tests checks that the actual package APIs match those files.
If we add new API to a package, the test breaks unless we add it to the API files.
And if we change or remove API, the test breaks too. This helps us avoid mistakes.
However, a tool like this only finds a certain class of problems, namely API changes and removals.
There are other ways to make incompatible changes to Go.
That leads us to the second approach we use to keep Go boring: testing.
## Testing {#testing}
The most effective way to find unexpected incompatibilities
is to run existing tests against the development version of the next Go release.
We test the development version of Go against
all of Google’s internal Go code on a rolling basis.
When tests are passing, we install that commit as
Google’s production Go toolchain.
If a change breaks tests inside Google,
we assume it will also break tests outside Google,
and we look for ways to reduce the impact.
Most of the time, we roll back the change entirely
or find a way to rewrite it so that it doesn’t break any programs.
Sometimes, however, we conclude that the change is
important to make and “compatible” even though it does
break some programs.
In that case, we still work to reduce the impact as much as possible,
and then we document the potential problem in the release notes.
Here are two examples of that kind of subtle compatibility problems
we found by testing Go inside Google but still included in Go 1.1.
## Struct Literals and New Fields {#struct}
Here is some code that runs fine in Go 1:
package main
import "net"
var myAddr = &net.TCPAddr{
net.IPv4(18, 26, 4, 9),
80,
}
Package `main` declares a global variable `myAddr`,
which is a composite literal of type `net.TCPAddr`.
In Go 1, package `net` defines the type `TCPAddr`
as a struct with two fields, `IP` and `Port`.
Those match the fields in the composite literal,
so the program compiles.
In Go 1.1, the program stopped compiling, with a compiler error
that said “too few initializers in struct literal.”
The problem is that we added a third field, `Zone`, to `net.TCPAddr`,
and this program is missing the value for that third field.
The fix is to rewrite the program using tagged literals,
so that it builds in both versions of Go:
var myAddr = &net.TCPAddr{
IP: net.IPv4(18, 26, 4, 9),
Port: 80,
}
Since this literal doesn’t specify a value for `Zone`, it will use the
zero value (an empty string in this case).
This requirement to use composite literals for standard library
structs is explicitly called out in the [compatibility document](/doc/go1compat),
and `go vet` reports literals that need tags
to ensure compatibility with later versions of Go.
This problem was new enough in Go 1.1
to merit a short comment in the release notes.
Nowadays we just mention the new field.
## Time Precision {#precision}
The second problem we found while testing Go 1.1
had nothing to do with APIs at all.
It had to do with time.
Shortly after Go 1 was released, someone pointed
out that [`time.Now`](/pkg/time/#Now)
returned times with microsecond precision,
but with some extra code,
it could return times with nanosecond precision instead.
That sounds good, right?
More precision is better.
So we made that change.
That broke a handful of tests inside Google that
were schematically like this one:
func TestSaveTime(t *testing.T) {
t1 := time.Now()
save(t1)
if t2 := load(); t2 != t1 {
t.Fatalf("load() = %v, want %v", t1, t2)
}
}
This code calls `time.Now`
and then round-trips the result
through `save` and `load`
and expects to get the same time back.
If `save` and `load` use a representation
that only stores microsecond precision,
that will work fine in Go 1 but fail in Go 1.1.
To help fix tests like this,
we added [`Round`](/pkg/time/#Time.Round) and
[`Truncate`](/pkg/time/#Time.Truncate) methods
to discard unwanted precision,
and in the release notes,
we documented the possible problem
and the new methods to help fix it.
These examples show how
testing finds different kinds of incompatibility
than the API checks do.
Of course, testing is not a complete guarantee
of compatibility either,
but it’s more complete than just API checks.
There are many examples of problems we’ve found
while testing that we decided did break the compatibility
rules and rolled back before the release.
The time precision change is an interesting example
of something that broke programs but that we released
anyway.
We made the change because the improved precision
was better and was allowed within the documented behavior of the function.
This example shows that sometimes, despite significant effort
and attention, there are times when changing Go means
breaking Go programs.
The changes are, strictly speaking, “compatible” in the sense
of the Go 1 document, but they still break programs.
Most of these compatibility issues can be placed
in one of three categories:
output changes,
input changes,
and protocol changes.
## Output Changes {#output}
An output change happens when a function gives a different output
than it used to, but the new output is just as correct as, or even more correct than,
the old output.
If existing code is written to expect only the old output, it will break.
We just saw an example of this, with `time.Now` adding nanosecond precision.
**Sort.** Another example happened in Go 1.6,
when we changed the implementation of sort
to run about 10% faster.
Here's an example program
that sorts a list of colors by length of name:
colors := strings.Fields(
`black white red orange yellow green blue indigo violet`)
sort.Sort(ByLen(colors))
fmt.Println(colors)
Go 1.5: [red blue green white black yellow orange indigo violet]
Go 1.6: [red blue white green black orange yellow indigo violet]
Changing sort algorithms often changes
how equal elements are ordered,
and that happened here.
Go 1.5 returned green, white, black, in that order.
Go 1.6 returned white, green, black.
Sort is clearly allowed to return equal results in any order it likes,
and this change made it 10% faster, which is great.
But programs that expect a specific output will break.
This is a good example of why compatibility is so hard.
We don’t want to break programs,
but we also don’t want to be locked in
to undocumented implementation details.
**Compress/flate.** As another example, in Go 1.8 we improved
`compress/flate` to produce smaller outputs,
with roughly the same CPU and memory overheads.
That sounds like a win-win, but it broke a project inside Google
that needed reproducible archive builds:
now they couldn’t reproduce their old archives.
They forked `compress/flate` and `compress/gzip`
to keep a copy of the old algorithm.
We do a similar thing with the Go compiler,
using a fork of the `sort` package ([and others](https://go.googlesource.com/go/+/go1.21.0/src/cmd/dist/buildtool.go#22))
so that the compiler produces the same results
even when it is built using earlier versions of Go.
For output change incompatibilities like these,
the best answer is to write programs and tests
that accept any valid output,
and to use these kinds of breakages
as an opportunity to change your testing strategy,
not just update the expected answers.
If you need truly reproducible outputs,
the next best answer is to fork the code
to insulate yourself from changes,
but remember that
you’re also insulating yourself from bug fixes.
## Input Changes {#input}
An input change happens when a function changes which inputs it accepts
or how it processes them.
**ParseInt.** For example, Go 1.13 added support for
underscores in large numbers for readability.
Along with the language change,
we made `strconv.ParseInt` accept the new syntax.
This change didn't break anything inside Google,
but much later we heard from an external user
whose code did break.
Their program used numbers separated by underscores
as a data format.
It tried `ParseInt` first and only fell back to checking for underscores if `ParseInt` failed.
When `ParseInt` stopped failing, the underscore-handling code stopped running.
**ParseIP.** As another example, Go’s `net.ParseIP`,
followed the examples in early IP RFCs,
which often showed decimal IP addresses with leading zeros.
It read the IP address 18.032.4.011 address as 18.32.4.11, just with a few extra zeros.
We found out much later that BSD-derived C libraries
interpret leading zeros in IP addresses as starting an octal number:
in those libraries, 18.032.4.011 means 18.26.4.9!
This was a serious mismatch
between Go and the rest of the world,
but changing the meaning of leading zeros
from one Go release to the next
would be a serious mismatch too.
It would be a huge incompatibility.
In the end, we decided to change `net.ParseIP` in Go 1.17
to reject leading zeros entirely.
This stricter parsing ensures that when Go and C
both parse an IP address successfully,
or when old and new Go versions do,
they all agree about what it means.
This change didn't break anything inside Google,
but the Kubernetes team was concerned about
saved configurations that might have parsed before
but would stop parsing with Go 1.17.
Addresses with leading zeros
probably should be removed from those configs,
since Go interprets them differently from
essentially every other language,
but that should happen on Kubernetes’s timeline, not Go’s.
To avoid the semantic change,
Kubernetes started using its own forked copy
of the original `net.ParseIP`.
The best response to input changes is to process user input
by first validating the syntax you want to accept
before parsing the values,
but sometimes you need to fork the code instead.
## Protocol Changes {#protocol}
The final common kind of incompatibility is protocol changes.
A protocol change is a change made to a package
that ends up externally visible
in the protocols a program uses
to communicate with the external world.
Almost any change can become externally visible
in certain programs, as we saw with `ParseInt` and `ParseIP`,
but protocol changes are externally visible
in essentially all programs.
**HTTP/2.** A clear example of a protocol change is when
Go 1.6 added automatic support for HTTP/2.
Suppose a Go 1.5 client is connecting to an HTTP/2-capable
server over a network with middleboxes that happen to
break HTTP/2.
Since Go 1.5 only uses HTTP/1.1, the program works fine.
But then updating to Go 1.6 breaks the program, because Go 1.6
starts using HTTP/2, and in this context, HTTP/2 doesn’t work.
Go aims to support modern protocols by default,
but this example shows that enabling HTTP/2 can break programs
through no fault of their own (nor any fault of Go's).
Developers in this situation could go back to using Go 1.5,
but that’s not very satisfying.
Instead, Go 1.6 documented the change in the release notes
and made it straightforward to disable HTTP/2.
In fact, [Go 1.6 documented two ways](/doc/go1.6#http2) to disable HTTP/2:
configure the `TLSNextProto` field explicitly using the package API,
or set the GODEBUG environment variable:
GODEBUG=http2client=0 ./myprog
GODEBUG=http2server=0 ./myprog
GODEBUG=http2client=0,http2server=0 ./myprog
As we’ll see later, Go 1.21 generalizes this GODEBUG mechanism
to make it a standard for all potentially breaking changes.
**SHA1.** Here's a subtler example of a protocol change.
No one should be using SHA1-based certificates for HTTPS anymore.
Certificate authorities stopped issuing them in 2015,
and all the major browsers stopped accepting them in 2017.
In early 2020, Go 1.18 disabled support for them by default,
with a GODEBUG setting to override that change.
We also announced our intent to remove the GODEBUG setting in Go 1.19.
The Kubernetes team let us know that
some installations still use private SHA1 certificates.
Putting aside the security questions,
it's not Kubernetes’s place to force these enterprises to
upgrade their certificate infrastructure,
and it would be extremely painful to fork `crypto/tls`
and `net/http` to keep SHA1 support.
Instead, we agreed to keep the override in place longer than we had planned,
to create more time for an orderly transition.
After all, we want to break as few programs as possible.
## Expanded GODEBUG Support in Go 1.21
To improve backwards compatibility even in these subtle cases
we’ve been examining, Go 1.21 expands and formalizes the use of GODEBUG.
To begin with, for any change that is permitted by Go 1 compatibility but
still might break existing programs,
we do all the work we just saw to understand potential
compatibility problems, and we engineer the change to keep as many existing
programs working as possible.
For the remaining programs, the new approach is:
1. We will define a new GODEBUG setting that allows
individual programs to opt out of the new behavior.
A GODEBUG setting may not be added if doing so is infeasible, but that should be extremely rare.
2. GODEBUG settings added for compatibility will be maintained for a minimum
of two years (four Go releases). Some, such as `http2client` and `http2server`,
will be maintained much longer, even indefinitely.
3. When possible, each GODEBUG setting has an associated
[`runtime/metrics`](/pkg/runtime/metrics/) counter
named `/godebug/non-default-behavior/<name>:events`
that counts the number of times a particular program’s behavior
has changed based on a non-default value for that setting.
For example, when `GODEBUG=http2client=0` is set,
`/godebug/non-default-behavior/http2client:events` counts the
number of HTTP transports that the program has configured without HTTP/2 support.
4. A program’s GODEBUG settings are configured to match the Go version
listed in the main package’s `go.mod` file.
If your program’s `go.mod` file says `go 1.20` and you update to
a Go 1.21 toolchain, any GODEBUG-controlled behaviors changed in
Go 1.21 will retain their old Go 1.20 behavior until you change the
`go.mod` to say `go 1.21`.
5. A program can change individual GODEBUG settings by using `//go:debug` lines
in package `main`.
6. All GODEBUG settings are documented in a [single, central list](/doc/godebug#history)
for easy reference.
This approach means that each new version of Go should be the best possible
implementation of older versions of Go, even preserving behaviors that were
changed in compatible-but-breaking ways in later releases when compiling old code.
For example, in Go 1.21, `panic(nil)` now causes a (non-nil) runtime panic,
so that the result of [`recover`](/ref/spec/#Handling_panics) now reliably
reports whether the current goroutine is panicking.
This new behavior is controlled by a GODEBUG setting and therefore dependent
on the main package’s `go.mod`’s `go` line: if it says `go 1.20` or earlier,
`panic(nil)` is still allowed.
If it says `go 1.21` or later, `panic(nil)` turns into a panic with a `runtime.PanicNilError`.
And the version-based default can be overridden explicitly by adding a line like this to package main:
//go:debug panicnil=1
This combination of features means that programs can update to newer toolchains
while preserving the behaviors of the earlier toolchains they used,
can apply finer-grained control over specific settings as needed,
and can use production monitoring to understand which jobs make
use of these non-default behaviors in practice.
Combined, those should make rolling out new toolchains even
smoother than in the past.
See “[Go, Backwards Compatibility, and GODEBUG](/doc/godebug)” for more details.
## An Update on Go 2 {#go2}
In the quoted text from “[Go 1 and the Future of Go Programs](/doc/go1compat)”
at the top of this post, the ellipsis hid the following qualifier:
> At some indefinite point, a Go 2 specification may arise,
> but until that time, [... all the compatibility details ...].
That raises an obvious question: when should we expect the
Go 2 specification that breaks old Go 1 programs?
The answer is never.
Go 2, in the sense of breaking with the past
and no longer compiling old programs,
is never going to happen.
Go 2 in the sense of being the major revision of Go 1
we started toward in 2017 has already happened.
There will not be a Go 2 that breaks Go 1 programs.
Instead, we are going to double down on compatibility,
which is far more valuable than any possible break with the past.
In fact, we believe that prioritizing compatibility
was the most important design decision we made for Go 1.
So what you will see over the next few years
is plenty of new, exciting work, but done in a careful,
compatible way, so that we can keep your upgrades from
one toolchain to the next as boring as possible.