How to patch the Go toolchain

Home - RSS

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:

Build a toolchain from source §

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.

Use the 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.

Bazel §

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.

Patch the sources from 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:

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.

Build-time overlay §

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:

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.

Orchestrion §

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.