A Claude Code skill and Go CLI for Go developers about to raise the go directive in go.mod, for example from go 1.21 to go 1.24. It lists the GODEBUG defaults that change, which of your binaries import the package each setting controls, and the godebug lines that keep the old behavior. The answers come from your installed Go toolchain, offline, without editing go.mod.
Since Go 1.21, the go line in your main module's go.mod sets the GODEBUG defaults your binaries are built with. Raising it switches net/http routing, TLS key exchange, x509 parsing, math/rand.Seed, GOMAXPROCS in containers and more to the new behavior, with no code change and no compile error. The list is spread across many sections of the GODEBUG history. godebugdiff asks the toolchain itself for each binary's defaults at both go lines and shows the difference. The skill then has Claude check whether your code uses each changed API.
- "I want to bump go.mod from go 1.21 to 1.24. What behavior changes?"
- "This Dependabot PR raises the go directive. Is anything going to break?"
- "After upgrading Go, our ServeMux routes return 404. Why?"
- "Which godebug lines do I need to keep the old TLS behavior after the bump?"
As a Claude Code plugin:
claude plugin marketplace add GillMacedo/godebugdiff
claude plugin install godebugdiff@godebugdiff-marketplace
Or as a CLI with Go 1.22 or newer. It has no dependencies:
go run github.com/GillMacedo/godebugdiff/plugin/scripts@latest --to 1.24 .
examples/muxcheck is a small program on go 1.21 that registers a Go 1.22 style route, GET /orders/{id}. From a clone of this repo:
$ go run ./plugin/scripts --to 1.22 examples/muxcheck
go 1.21 -> 1.22 in example.com/muxcheck (go1.27.3, 1 binary)
2 GODEBUG defaults change. 2 reach a package your binaries import.
httplaxcontentlength 1 -> default Go 1.22, net/http
imported by example.com/muxcheck
Go 1.22 made it an error for a request or response read by a net/http client or server to have an empty Content-Length header.
https://go.dev/doc/godebug#go-122
httpmuxgo121 1 -> default Go 1.22, net/http
imported by example.com/muxcheck
Go 1.22 changed the behavior of ServeMux to accept extended patterns and unescape both patterns and request paths by segment.
https://go.dev/doc/godebug#go-122
To keep the old behavior of a setting after the bump, pin it in go.mod:
godebug (
httplaxcontentlength=1
httpmuxgo121=1
)
Note: Crossing Go 1.22 also changes the language: each loop iteration gets its own loop variable in this module's code. That isn't a GODEBUG setting, so it isn't listed above. See https://go.dev/blog/loopvar-preview.
Built at go 1.21, that program answers GET /orders/42 with 404, because httpmuxgo121=1 treats the pattern as a literal path. Built at go 1.22 it answers 200. tests/ builds and runs it both ways to check this.
examples/shop has two binaries: an HTTP service and a batch job that only imports fmt and math/rand. Going from 1.21 to 1.24 changes 15 defaults. The full output is in examples/shop-1.21-to-1.24.txt, and it marks which binary each one reaches:
httpmuxgo121 1 -> default Go 1.22, net/http
imported by example.com/shop/cmd/api
...
randseednop 0 -> default Go 1.24, math/rand
imported by all 2 binaries
Go 1.24 changed the global math/rand.Seed to be a no-op.
A module whose go.mod already pins its defaults with godebug default=go1.21 stays quiet:
$ go run ./plugin/scripts --to 1.27 examples/pinned
go 1.21 -> 1.27 in example.com/pinned (go1.27.3, 1 binary)
No GODEBUG defaults change.
Note: Crossing Go 1.22 also changes the language: each loop iteration gets its own loop variable in this module's code. That isn't a GODEBUG setting, so it isn't listed above. See https://go.dev/blog/loopvar-preview.
Note: go.mod already pins default=go1.21. Pinned settings don't change with the go line, so they aren't listed.
On a real project: age at commit b74dce4, from its go 1.25.0 to 1.27, has 8 binaries. 4 of the 6 changed defaults reach one of them, and x509sslcertoverrideplatform and urlstrictcolons reach only cmd/age. Output from five open-source projects is in examples/real/.
godebugdiff [--from VERSION] [--to VERSION] [--tests] [--json] [--fail] [DIR]
DIRis the module root, the directory with go.mod. It defaults to..--todefaults to the installed toolchain's version, such as 1.27 for go1.27.3. It can't be newer than the toolchain, because older toolchains don't know about later settings.--fromdefaults to the go line in go.mod. Pass the base branch's version to review a change that already raised it.--testsadds test binaries. A module with no main package, such as a library, is checked through its test binaries automatically.--jsonprints the full result.--failexits 1 when a change reaches a package one of your binaries imports. Exit code 2 means bad usage, or the go command couldn't load the module.
In CI, on a pull request that changes go.mod:
base=$(git show origin/main:go.mod | awk '$1 == "go" { print $2 }')
go run github.com/GillMacedo/godebugdiff/plugin/scripts@latest --from "$base" .
It copies go.mod and go.sum to a temporary directory twice, sets the go line with go mod edit -go, and runs go list -modfile on each copy with GOPROXY=off and GOTOOLCHAIN=local. go list reports each main package's DefaultGODEBUG and its imports, so the result comes from the same code that sets defaults in go build. Package names and versions come from the toolchain's own table in $GOROOT/src/internal/godebugs, and the one-line descriptions from $GOROOT/doc/godebug.md. Existing godebug lines in go.mod and //go:debug directives are applied by the toolchain, so pinned settings don't show up.
- Imported isn't the same as used. A binary that imports
net/httpgets thehttpmuxgo121change listed even if it never creates a ServeMux. The skill has Claude search the code for the API each setting names. - Only GODEBUG defaults. Other changes between Go releases, in the release notes, aren't covered. The language change to per-iteration loop variables in Go 1.22 is noted when you cross it, but not analyzed.
- Only what your toolchain knows. Settings removed from the toolchain no longer appear, and their new behavior already applies whatever your go line says. Use the newest toolchain you plan to build with.
- Needs the module cache or a vendor directory. It never downloads. If modules are missing it stops and says to run
go mod download. - Crossing Go 1.17 adds requirements. If the new go line needs
go mod tidychanges, it makes them in the temporary copy and says so. Your go.mod isn't touched. - go.work is ignored. In workspace mode the go line in go.work sets the defaults. The output says so when a go.work file is active.
- Descriptions need
$GOROOT/doc/godebug.md. Toolchains that the go command downloads by itself, throughGOTOOLCHAIN, don't includedoc/, so they show only the link for each setting. - Descriptions are the first sentence of the matching paragraph in Go's own docs, and some settings were backported, so a description can name a later release than the one in the "Go 1.N" column. The link goes to the full text.
What is GODEBUG and why does the go line in go.mod change it? GODEBUG settings control behavior changes in the standard library and runtime. Since Go 1.21, a binary's defaults follow the go line of its main module, so raising it opts the binary into newer behavior. See Go, Backwards Compatibility, and GODEBUG.
Is it safe to upgrade the Go toolchain without changing go.mod? Mostly. With the same go line, a newer toolchain keeps the old defaults for settings it still has. Settings that were removed are the exception. godebugdiff covers the go line change.
How do I keep the old behavior after raising the go line? Add a godebug block to go.mod with the old values. godebugdiff prints that block for every change that reaches a binary. These settings are removed in later Go releases, so treat pins as temporary.
Why did my ServeMux routes start returning 404, or start matching differently, after a Go upgrade? httpmuxgo121 changed in Go 1.22. With the old value, patterns like GET /items/{id} are literal paths. See examples/muxcheck.
Does my library's go line change GODEBUG for programs that use it? No. Only the main module's go line counts. Raising a library's go line does force those programs to need that Go version, which raises their go line too.
Is it an MCP server? No. It's a skill with one Go source file, so go run is all it needs.
- Example modules in
examples/with Go 1.27.3 on Linux arm64. The integration tests intests/check that the settings listed forexamples/shopmatch theDefaultGODEBUGthatgo buildrecords in each binary (read withgo version -m), at both go lines. - A real behavior change:
examples/muxcheckbuilt at go 1.21 returns 404 and at go 1.22 returns 200, andgodebugdifflistshttpmuxgo121for it. - Healthy cases stay quiet:
examples/pinnedwithgodebug default=go1.21, and any module compared at the same version, print "No GODEBUG defaults change." - Real projects, to the installed 1.27: hey
5626f79(go 1.24.0), fzf26ff936(go 1.23.0), ageb74dce4(go 1.25.0, 8 binaries), cobraadbc881(a library on go 1.15, which needed the temporary tidy) and rclonef0cb5dd(which pins two settings in go.mod). Outputs are inexamples/real/. A vendored copy of hey gave the same result with an empty module cache, and hey without a vendor directory and with an empty cache stopped with the "run go mod download" message. None of these checked whether the projects use each changed API. - Go 1.22.12, a downloaded toolchain: the unit tests and the ServeMux test pass, and the tests that need a newer toolchain skip. On
examples/muxcheckit also liststls10server,tlsrsakexandtlsunsafeekm, which Go 1.27 removed, and shows links without descriptions. - Errors: a
godebugline with a removed setting,--tonewer than the toolchain, and a directory with no go.mod.
It hasn't been tested on Windows or macOS, on toolchains other than 1.22.12 and 1.27.3, with go.work workspaces beyond the note, in a fresh Claude session, or through go run ...@latest, which needs the published repo.
Contributions are welcome, from people and from people working with AI agents. See CONTRIBUTING.md. Good first issues are listed here.
- Open a PR. Fork the repo, work on a branch, and keep it to one change. Write a title that says what changes for users, because it becomes the changelog line:
feat:goes under Added andfix:under Fixed. - Automatic check. Code changes need tests, and
make testmust pass. Changes over 30 lines need a linked open issue (Closes #<number>), and a PR can have at most 800 changed lines. If something is missing, a comment tells you what to fix. - Review. Claude reviews each PR automatically. Code PRs also get a security review and are tested in a sandbox. The review then merges the PR or asks for changes. PRs that change CI, agent settings, npm or Python dependencies, vendored code or build files can't be merged this way, and a comment says so. The same goes for anything the security review doesn't clear.
- Release. A merge that changes what users install becomes a new version within about two hours, with a GitHub Release and, if the project has one, its npm package. Docs and CI changes wait for the next release.
This project was largely generated with Claude Code and reviewed by its maintainer.
I'm Giulliano Macedo, and I build developer tools at GoTech Solutions. Follow along or get in touch:
- GitHub: GillMacedo
- LinkedIn: giulliano
- X: @giumacedo
If godebugdiff saved you some prompts, a star helps others find it.
Built by Giulliano Macedo, GoTech Solutions · LinkedIn · X