Recently I ran into a curious behavior related to the go directive in the go.mod file. It’s well documented, but it’s nevertheless surprising.

Go 1.11 introduced support for modules, as a way to define a collection of related Go packages. You can find details about its use and operation in the documentation. This support has continued to evolve throughout 1.12 and 1.13.

A module is defined using a go.mod file, described here. A minimal file looks like this:

module my-module

That’s enough to define the directory containing that file as a Go module and establish the module’s path.

Typically, a go.mod file will include dependencies, like this:

module my-module

require (
	example.org/m v1.0.1
	github.com/pkg/errors v0.8.1
)

That says that specific versions of the named packages are required by this module.

The go.mod file can include a go directive, which establishes the expected language version, like this:

module my-module

go 1.11

This directive is not required, but if it’s not specified, the version of the compiler being used is assumed.

If you use go mod init to create the go.mod file, the directive will be added, and it will have the same version of the go tool that you are currently using (if you use go 1.12, it will read go 1.12 and so on).

The documentation says that it “determines which language features are available when compiling the module”. That terse wording is what caught me by surprise.

Consider a simple example:

package main

import "fmt"

func main() {
    fmt.Printf("The answer is %d\n", 42)
}

That builds and runs fine with any version of Go, but since I used go 1.13 to create the go.mod file, it looks like this:

module version-1

go 1.13

Running the program with Go 1.13, you get the expected output:

$ go version
go version go1.13 linux/amd64

$ go run .
The answer is 42

What would you expect to happen if you try to build this with go 1.12?

$ go version
go version go1.12.9 linux/amd64

$ go run .
The answer is 42

In the much more complex context where I ran into this, I was fully expecting the line to mean fail unless built using go 1.13 or later. This is how the verb used to behave with 1.11 throught 1.11.3. It was changed in 1.11.4 to be compatible with the same change introduced in 1.12.

Reading the quoted documentation again, note that it says the go verb “determines which language features are available when compiling the module”. It talks about features, not about the version of the compiler.

Consider this change to understand what that means:

package main

import "fmt"

func main() {
    fmt.Printf("The answer is %d\n", 0b101010)
}

the code still works fine with go 1.13. Now it does this with 1.12:

$ go version
go version go1.12.9 linux/amd64

$ go run .
# version-2
./version.go:6:36: syntax error: unexpected b101010, expecting comma or )
note: module requires Go 1.13

The compiler produces an error, because it doesn’t know about the new syntax, and it outputs a note about requiring Go 1.13. It doesn’t “know” which version is required, it’s simply stating the information stored in the go.mod file.

If the go.mod file is changed to specify Go 1.12, the error remains but the note goes away:

$ go version
go version go1.12.9 linux/amd64

$ go run .
# version-3
./version.go:6:36: syntax error: unexpected b101010, expecting comma or )

And using Go 1.13:

$ go version
go version go1.13 linux/amd64

$ go run .
# version-3
./version.go:6:35: binary literals only supported as of -lang=go1.13

The gist of it is:

  • The go verb in go.mod specifies which language features are available to the code. This does not include compiler flags (e.g. -trimpath).
  • Building with an older version is possible as long as the code does not use features introduced with the newer version.
  • Building with an older version will produce a syntax error if the code uses newer features, and a hint is provided as to the version that is required based on the contents of go.mod.
  • Building with a compiler version that does support the features in the code with a go.mod file specifying an older version will not produce a syntax error but it will cause an error indicating the specific version required to build the code.