Go and the promise of backward compatibility
When version 1.0 of the Go language was released, its developers made a commitment to maintain backward compatibility as new versions came out.
This commitment brings several interesting results. For example, the code you learn doesn’t become outdated. On top of that, keeping the language backward compatible reinforces another important commitment: simplicity and readability of code. That project that sat untouched for months, maybe years, still compiles on the first try. I experienced this personally with code that sat in a drawer for three years.
Despite these efforts, it isn’t always possible to maintain absolute compatibility for so many years. Security flaws, for instance, sometimes need to be reflected in the code, and small changes may be introduced. For those rare situations, there is the go fix command. And even then it is rarely used. I remember using go fix only once.
For example, the go fix command replaces the old +build build tag with the new go:build. In the file, the line // +build !windows is replaced by //go:build !windows.
Example usage
go fix ./...
luaengine/setwinsize_unix.go: fixed buildtag
luaengine/setwinsize_windows.go: fixed buildtag
term/resizeterm_unix.go: fixed buildtag
term/resizeterm_windows.go: fixed buildtag
Limitations
Beyond security fixes, compatibility is at the source level, not the binary level. So even though the code keeps compiling and behaving as expected, it may produce different binaries.
How incompatibilities are discovered
The Go team does some interesting things to ensure backward compatibility. First, they keep a list of every function exported by the standard library. When they test a new version, they compare the old list with the new one, and that tells them whether any compatibility was broken.
Another interesting way compatibility is validated is through tests. Before a new version is released, it is tested against Google’s entire Go codebase, and they assume that if compatibility breaks at Google, it will break for everyone else too.
Keeping code extensible
Go is careful not to break the API when it comes to structs and interfaces. One interesting approach, for example, is to use named parameters whenever you populate a struct. That way, if you add new fields, old code that was populated with named fields will keep compiling.
For example, consider the following struct:
type valores struct {
a int
b int
}
The code below will compile just fine.
val1 := valores{a: 1, b: 2}
val2 := valores{1, 2}
If in the future you decide you need one more field and change the struct as shown below:
type valores struct {
a int
b int
c int
}
The val1 line will keep compiling and working as expected, but val2 will no longer compile.
Since Go automatically initializes values, unpopulated fields have a predictable value. Even pointers will be nil, and they’ll be easy to handle in the code.
Fork Go (or parts of it)
As a last resort, one alternative to extend your program’s longevity is to fork the parts that, for whatever reason, lost compatibility with the version you’re using.
This is a somewhat radical option, but sometimes necessary, especially if you’re using third-party libraries. The problem with it is that your code keeps compiling, but you won’t receive security updates and improvements. You’ll have to take on the responsibility of maintaining that new chunk yourself.