2020-07-18
Using Go build directives to optionally use new APIs in the standard library
I mentioned recently that new APIs in the Go standard library were relatively easy to optionally support, because such new APIs only appear in new Go releases and you can conditionally build files based on the Go release that's building your program. But that's a pretty abstract description, so let's make it concrete.
One of the long time limitations of the crypto/tls
package was that it only gave
you numbers for TLS cipher suites, not
any sort of names, and for logging and reporting things to users
you often wanted a name. Go's lack of names for its cipher suite
numbers left you to roll your own conversion, generally with a
big table of mappings, as I
do (or did) in my call
program. In Go 1.14, the Go authors fixed this by adding
tls.CipherSuiteName()
. If I was
content to have the latest version of call
only build on Go
1.14 or later, I could simply convert it to directly using
tls.CipherSuiteName()
in place of my current hand done solution.
However, for various reasons I would like to keep call
building
on previous Go versions as well, which means that I need to use my old
solution if the new API isn't available.
The first step is to pull out the small function that needs to
access the API into a separate file, or create a function to do
this if you don't have one already. In my case I might as well call
my (new) function cipherSuiteName()
. Then we create two versions
of this function in two files, let's call them ciphername.go
and
ciphername_old.go
. The second file has the current implementation,
for older versions of Go, while the first version has the new
implementation that calls tls.CipherSuiteName()
.
The first file can very simple if all you want to do is directly call the new API and accept its result. It needs a Go build directive to only build on Go 1.14 and later, and can look like this:
// There must be a blank line between the // build directive and 'package main'. // // +build go1.14 package main import "crypto/tls" func cipherSuiteName(id uint16) string { return tls.CipherSuiteName(id) }
The second file will have a more complicated implementation, which I'm leaving out here, and a Go build directive to not build on Go 1.14 (and later):
// For pre 1.14 Go versions without // tls.CipherSuiteName(). // // +build !go1.14 package main import "fmt" func cipherSuiteName(id uint16) string { .... }
My view is that you should use the longer file name for the old
implementation, because in the long run you're probably going to
delete it (when you stop supporting old Go versions). Depending
on what your function in ciphername.go
does, you might want to
keep it or simply switch over to a direct call to
tls.CipherSuiteName()
.
Useful references for Go build directives are the go/build package documentation and Dave Cheney's How to use conditional compilation with the go build tool.
(This is the kind of entry that I write partly for my later use, because I'm sure I'm going to want to do this for other APIs in the future.)