Posted 2025-07-02
Every now and then, I've run into a tricky Go bug that seems to involve the compiler, runtime, or standard library. Try as I might, I can't reproduce the bug. I've only seen it happen in production. I don't have enough information to diagnose the issue. Sometimes I've wished I could get the runtime to emit some more diagnostics, or just wanted to try out a potential fix from another Go contributor. This post covers a few ways you can build your Go programs with changes applied to the compiler, runtime, or standard library.
The short version:
go-patch-overlay
to apply patches at build time.
The first method is to build a custom Go toolchain,
and use it to build your program.
By toolchain, I mean the collection of tools (go
command, compiler, linker, etc),
along with the runtime and standard library sources.
There are ways to change the runtime and standard library sources when you build your program.
But if you want to change the tools,
you'll need to build them ahead of time.
You first check out the Go source code:
git clone https://go.googlesource.com/go
If you're making a change to a specific Go release version,
use git switch --detach {release tag}
to check it out.
Then you make the changes you want.
If you want to build the toolchain to use locally, there's a script to build it:
cd src ./make.bash
Then add /path/to/go/bin
to your PATH
.
After that, running the go
command will use the toolchain you built.
What if you want to give this toolchain to someone else, or use it in your CI? My colleague Nayef has done this at Datadog. Here's how he's done it:
You'll need a VERSION
file to make a distribution:
echo go1.x.y > VERSION
Replace x
and y
with numbers.
For example,
if you were testing a fix for go1.24.4
,
you could do go1.24.999
.
That'll give you a valid version that's also unlikely to collide with a real Go version.
Then you'll actually build the distribution:
cd src/ GOOS=linux GOARCH=amd64 ./make.bash -distpack
Set GOOS
and GOARCH
as needed.
You'll end up with a distribution like
../pkg/distpack/go1.X.Y.linux-amd64.tar.gz
To actually use this,
you'll essentially follow the steps in the Go installation docs.
Copy go1.X.Y.linux-amd64.tar.gz
to wherever you want to install it,
and then extract it.
You'll need to update the PATH
environment variable
to include the bin
subdirectory of the resulting directory.
Then when you run the go
command you'll be using the custom toolchain.
gotip
tool §
If you have a CL you'd like to use (from go.dev/cl),
there's a tool that can build the toolchain for you.
It's called gotip
.
This might apply to you if you've reported a bug,
and a Go maintainer has a potential fix for you to try.
The gotip
readme explains it,
but here's a concrete example:
Say you want to try out go.dev/cl/12345.
The "CL number" is 12345.
You can then use gotip
like so:
go install golang.org/dl/gotip@latest gotip download 12345
This will download the Go sources from that CL number,
and then build the Go toolchain from those sources.
Then you can use gotip
to build your program in place of the normal go
command.
If you build your Go code with Bazel,
then ignore the rest of this post and just refer to this section.
Full disclosure:
At the time of writing this post,
I haven't actually used this method.
I don't know if this works for compiler/linker/etc patches or just runtime/library code.
But we have some Bazel stuff at Datadog so it seemed worth mentioning here.
The rules_go
rules support patching the tools and libraries out of the box.
It's described here.
Here's an example straight from the manual:
go_download_sdk( name = "go_sdk", goos = "linux", goarch = "amd64", version = "1.18.1", sdks = { "linux_amd64": ("go1.18.1.linux-amd64.tar.gz", "b3b815f47ababac13810fc6021eb73d65478e0b2db4b09d348eefad9581a2334"), "darwin_amd64": ("go1.18.1.darwin-amd64.tar.gz", "3703e9a0db1000f18c0c7b524f3d378aac71219b4715a6a4c5683eb639f41a4d"), }, patch_strip = 1, patches = [ "//patches:cgo_issue_fix.patch", ] )
I've bolded the relevant part. You just provide a list of patches, and they're applied when Bazel builds and installs the toolchain.
As mentioned previously,
if you want to change the compiler, linker, or other tools,
you'll need to rebuild them.
But if your changes are just to the runtime or standard library,
you have other options.
Your Go toolchain includes the runtime and standard library source code.
The code for your toolchain is under the toolchain's GOROOT
:
% go env GOROOT /usr/local/go % go1.24.4 env GOROOT /Users/nick/sdk/go1.24.4
There are a few ways you can modify the source code. The first way is to edit it directly. You can use a text editor, or if you have a patch, you can apply it to the source code. Don't do this to your local toolchain installation! It's a pain to undo your changes. This method is suitable for doing during a build in a Docker container.
There are a few ways to get a patch:
git diff > change.patch
CL_NUMBER=12345 curl -XGET "https://go-review.googlesource.com/changes/go~${CL_NUMBER}/revisions/current/patch?raw" > ${CL_NUMBER}.patch
Assuming you have a patch, you can apply it to the Go toolchain sources like so:
pushd $(go env GOROOT) git apply /path/to/patch1.diff /path/to/patch2.diff ... popd go build ...
Whatever changes you make to the sources will be reflected in the program you build.
Expect your build to be a bit slower if you change widely depended-upon packages,
like runtime
.
Turns out there's another way to change the Go sources at build time,
without needing to actually modify your installation.
The go build
command accepts an -overlay
argument.
The argument is a path to a JSON file like this:
{ "Replace": { "/path/to/source1.go": "/other/path/to/replacement.go", "/path/to/source2.go": "", ... } }
Basically, an overlay is a key-value map where the keys are file paths seen by the compiler,
and the values are replacement files.
This can even include new or deleted files.
The overlay feature was originally added to support gopls
,
where the language server needs to tell the compiler about files whose contents haven't been written to disk.
We can use it to try out patches.
There's a tool that can generate an overlay for you from patches:
go-patch-overlay
.
My colleague Felix wrote this a few years ago.
I wrote a very similar program during a recent team research week with a few fixes and extra features,
before I knew about Felix's program,
so we incorporated my changes into his program.
You use the tool like this:
go install github.com/felixge/go-patch-overlay@main go build -overlay=$(go-patch-overlay /path/to/patch.diff)
It works like this:
go-patch-overlay
command reads the patch and determines which files it modifies.
go env GOROOT
to find where the toolchain sources are installed.
It copies any modified files into a new temporary directory.
GOROOT
.
The file replacements in the overlay only apply to that specific go build
invocation.
Your toolchain's sources are unchanged.
You can also pass the -overlay
argument to go run
and go test
.
I personally like this method the best for runtime/standard library changes. It's lightweight, non-destructive, and easy to use both locally and in CI.
There's one more tool worth mentioning here:
Orchestrion.
Orchestrion automatically instruments Go code.
It was originally built for distributed tracing, profiling, and security monitoring instrumentation.
It works by rewriting source code at build time.
It's a really powerful tool,
kind of a low orbit ion cannon compared to the hammer of applying a patch.
I won't go into the details here.
If the changes you want to make are simple,
I'd start with patches and go-patch-overlay
.
But keep Orchestrion in mind if you need something more powerful.