A Go module can compile on a developer workstation and still make a promise it cannot keep. The usual cause is a newer toolchain: an import or API lands in a change while go.mod continues to declare the oldest Go release supported by the project. A CI runner using that older release then fails after the change has already moved through review.
Go 1.27 makes that mistake easier to spot. Its default go test vet set includes stdversion, which reports a standard-library symbol that is newer than the module version in force. This is useful during an upgrade because it turns an implicit compatibility assumption into a check that can run with the ordinary test suite.
This article builds a small disposable module to show the failure and the correction. The successful result is not merely that Go 1.27 accepts the code. It is that the module declaration and the selected standard-library API agree, so the repository tells CI and downstream users the same version story.
Start with the version contract
The go line in go.mod is more than a note about the tool installed on a laptop. It declares the language and module behavior the project expects. It is not a substitute for testing every supported compiler, but it is the right place to state the intended minimum Go release.
Go 1.27 introduced the standard-library uuid package. It provides UUID generation and parsing and uses a cryptographically secure random-number generator for the random components of new UUIDs. A project that wants to use uuid.New therefore needs a Go 1.27 boundary, or it needs to keep using a dependency or an existing implementation compatible with its stated older boundary.
Create an empty directory outside an existing repository. The example deliberately says Go 1.26 in go.mod while importing an API that arrived in Go 1.27:
go.mod
uuid_test.go
module example.com/stdversion-check
go 1.26
package stdversioncheck
import (
"testing"
"uuid"
)
func TestUUIDIsAvailable(t *testing.T) {
if got := uuid.New().String(); len(got) != 36 {
t.Fatalf("unexpected UUID length: %d", len(got))
}
}
The test is intentionally uninteresting. It only gives the compiler a real use of uuid.New; the compatibility diagnostic is the point of the exercise. Do not copy the module path into a project. Replace it with the module’s own path.
Run the check with the upgrade toolchain
First confirm which binary is about to test the module. In a repository, use the same pinned Go binary that the CI upgrade job will use rather than whatever go resolves from a shell profile.
go version
go test .
The disposable run used Go 1.27.0 on Linux amd64. With go.mod still declaring 1.26, the result was:
# example.com/stdversion-check
# [example.com/stdversion-check]
./uuid_test.go:9:17: uuid.New requires go1.27 or later (module is go1.26)
FAIL example.com/stdversion-check [build failed]
FAIL
That failure is the useful result. It identifies the symbol, source location, required version, and declared version. It is narrower than a general compiler error and avoids the false comfort of saying that an application happened to compile under the newest workstation toolchain.
stdversion works from the version in force for the file. In ordinary code that comes from the go directive. Build constraints can set a version for a particular file, so do not treat a single passing package as proof that every platform-specific file has the same compatibility boundary. Run the normal test matrix as well.
The check is also not an upgrade planner. It does not decide whether a dependency, base image, deployment platform, or customer environment can move to Go 1.27. It only answers a precise question: does this source use a standard-library API newer than the version it declares?
Correct the mismatch deliberately
There are two valid fixes, and they represent different support decisions.
If the project is ready to require Go 1.27, make the change explicit in go.mod:
module example.com/stdversion-check
go 1.27
Then rerun the same command:
go test .
The local retest completed successfully:
ok example.com/stdversion-check 0.001s
Commit the go.mod change with the code that needs the newer API. Keeping them together makes it clear why the minimum version changed. Update the CI image, release documentation, container builder, and any version-check workflow in the same review. A declaration that says 1.27 while CI still runs 1.26 just trades one confusing failure for another.
If the project must continue to support Go 1.26, do not raise the go line simply to silence the diagnostic. Remove the new standard-library use or choose an implementation that is documented to support the actual minimum version. Then test with that minimum compiler. This is especially important for command-line tools and libraries used across teams, where the toolchain may be controlled by a distribution image rather than the application repository.
Add it to an upgrade check
Most projects do not need a separate command to get this coverage: go test ./... with Go 1.27 runs the default vet checks. A small CI job can make the boundary visible during upgrade work:
- name: Test declared Go compatibility
run: go test ./...
Use a pinned Go 1.27 runner for this job. Keep a separate job for the oldest Go release the project claims to support. The newer job catches accidental references to APIs beyond the declared boundary; the oldest job verifies the practical promise by compiling and running the suite with that compiler.
For a module with generated code, run generation before testing only if that is the repository’s normal build order. Generated output can introduce imports that are absent from handwritten files. For multi-module repositories, run the command from each module root or have CI enumerate the module list. A top-level invocation does not automatically establish a compatibility result for every nested go.mod.
Before changing the version line, inspect the diff from go mod tidy. Go 1.27 also consolidates duplicate require blocks for modules declaring Go 1.27 or later. That cleanup may be legitimate, but it is separate from choosing a new minimum version. Review it as a formatting and dependency-metadata change, not as proof that the application is compatible.
Verify the outcome readers will depend on
A useful upgrade review has three checks:
- The version in
go.modmatches the minimum Go release the project is willing to support. go test ./...passes on the new, pinned Go release withoutstdversionfindings.- The same test suite passes on the documented minimum Go release when that minimum remains below the new toolchain.
The first two checks catch the mismatch demonstrated here. The third catches code paths, build tags, and tool behavior that a newer compiler cannot represent. Add a build of the distributable binary as well when the repository produces one; a test-only dependency graph can differ from the final command.
Go 1.27’s diagnostic does not eliminate compatibility testing, but it supplies a clear early failure for a common upgrade error. Treat the go directive as a maintained support contract, run the default test command on the target toolchain, and change the contract only when the rest of the delivery path is ready to honor it.