3.3 Bootstrapping the Language
The Go compiler is written in Go. To compile it, one first needs a Go compiler, and then the question is who compiled that one.
Start with an experiment. Take the source of Go 1.26.10 (with the precompiled programs in the
release package removed) and copy it twice. Build one copy with an installed Go 1.26.0 and the other
with Go 1.27.3. make.bash is the script that builds Go from source, and the environment variable
GOROOT_BOOTSTRAP names the existing Go to use as the starting point (darwin/arm64, paths
abbreviated, about 40 seconds per build):
| |
The two builds produce 10 programs in total under bin and pkg/tool, and a byte-by-byte
comparison shows that all are identical:
| |
The starting compilers differ, yet the products are exactly the same. This section answers three questions: where the first Go compiler came from; what toolchain1, 2 and 3 in the output above are and why the result does not depend on the starting point; and why this property matters.
3.3.1 The First Compiler
A compiler involves three languages: the source language it processes (Go), the target language it produces (for example arm64 machine code), and the language in which it is implemented. The first two determine what the compiler does; the third determines how one gets the compiler in the first place.
Up to Go 1.4 (December 2014), the compiler, assembler and linker were all written in C, derived from
the Plan 9 C compiler (2.1). Building Go required only a C compiler on the
system, and either gcc or clang would do.
Go 1.5 (August 2015) reimplemented all of them in Go [Go Authors, 2015]. The conversion was not a rewrite: Russ Cox wrote a program that translated the C code into Go automatically. The translated code at first kept the style of C and was then gradually rewritten into idiomatic Go. In a 2013 design document, Cox gave the reasons: correct programs are easier to write in Go than in C, and Go comes with testing and profiling tools; the runtime was being migrated from C to Go, and a compiler written in Go would cooperate with it more easily; and contributors would need to know only one language [Cox, 2013].
The cost is that building Go now requires a working Go first [Cox, 2015]. Compiling a compiler with the compiler of the same language is called bootstrapping, a word that comes from the English idiom “to pull oneself up by one’s bootstraps”.
3.3.2 Building the New Version from an Old One
The output of make.bash shows the whole bootstrapping process. Below, the existing Go used as the
starting point is called the old Go:
- The old Go first compiles
cmd/dist, a small program that orchestrates the remaining steps and uses only language features the old Go also supports. - toolchain1: the old Go compiles the source of the new version’s compiler, assembler and linker. The behavior of the resulting compiler is determined by the new source, but its own machine code was generated by the old compiler.
- go_bootstrap: toolchain1 compiles the new
gocommand, which takes over the builds from here on. - toolchain2: toolchain1 compiles the same source again.
- toolchain3: toolchain2 compiles it once more.
- toolchain3 compiles the standard library and the remaining commands.
Step 4 is the key. The code a compiler generates depends only on its source and not on which compiler compiled it. The behavior of toolchain1 is determined by the new source, so toolchain2 is the result of “the new compiler compiling the new source”, and the influence of the old Go ends there.
make.bash -d saves the compiler of each round as compile1, compile2 and compile3. The
results of the two builds are:
| Starting from Go 1.26.0 | Starting from Go 1.27.3 | |
|---|---|---|
compile1 (compiled by the old Go) | f04179ffde503a8f | 21b1c7c8410a0c79 |
compile2 (compiled by toolchain1) | f91490ecbac04f2c | f91490ecbac04f2c |
compile3 (compiled by toolchain2) | 115ccaac31d7e4d4 | 115ccaac31d7e4d4 |
compile1 differs with the starting point, and from compile2 on the results are identical.
compile2 and compile3 differ because of a compile option. The compiler’s source directory holds
a profile of the compiler itself, default.pgo, which guides optimization (3.2).
When cmd/dist builds toolchain2, it deliberately turns this option off; a comment in the source
gives the reason that toolchain2 does not need PGO [Go Authors, 2026b]. It
is turned on when toolchain3 is built. Compile the compiler once more with the final compiler and
the same options (here go is the Go 1.26.10 just built):
| |
The result is still itself. Compiling again with the profile turned off (-pgo=off) gives exactly
f91490ecbac04f2c, the compile2. A compiler that compiles its own source and gets itself back is
said to have reached a fixed point: however many more times it is compiled, the result does not
change. If a compiler cannot generate itself stably, its output depends on something it should not,
such as the time or a memory address. GCC likewise uses a three-stage process when built from source
and compares the products of the last two stages [GCC, n.d.].
Since toolchain2 is already independent of the old Go, why is toolchain3 needed? A comment in
cmd/dist explains that the reason concerns the build ID introduced in 3.1
[Go Authors, 2026b]. toolchain1 is built by the go command of the old Go
and contains no build ID, so a toolchain2 with a build ID has to be built. In a development version,
however, the build ID of the toolchain enters the computation of the build IDs of its products, so
once the compiler is replaced, the go command considers everything built before to be out of date.
A release version instead substitutes the version number reported by the toolchain for the build ID,
and the problem does not arise. To make the two cases behave alike, cmd/dist uses toolchain2 to
force a rebuild of the whole set of tools, which yields toolchain3, whose compiler and linker carry
accurate build IDs that the cache can rely on. At the end, make.bash also has the go command
check, one by one, that from its point of view the toolchain and the standard library are all up to
date and need no rebuild.
The comment at the top of buildid.go explains why the design in 3.1 is necessary here: the
compiler is compiled by itself, so if the key of the next step recorded the action ID of the
compiler step, the key would change every time the compiler compiled itself and never converge; if
it records the digest of the compiler’s content, then once the content converges, the key converges
with it.
3.3.3 The Minimum Version of the Starting Point
The Go used as a starting point has a minimum version, which rises over time with the language features and libraries the new source uses [Go Authors, 2026a]:
| Version to build | Minimum requirement for the starting point |
|---|---|
| Go 1.4 and earlier | a C compiler |
| Go 1.5 to 1.19 | Go 1.4 |
| Go 1.20, 1.21 | Go 1.17.13 |
| Go 1.22, 1.23 | Go 1.20.6 |
| Go 1.24, 1.25 | Go 1.22.6 |
| Go 1.26, 1.27 | Go 1.24.6 |
Go 1.4 served as the starting point for nearly eight years. It is the last version written in C, and
any machine with a C compiler can begin building from it. The cost is that the compiler itself could
never use features introduced after Go 1.4, such as generics; Go 1.4 also does not support some
platforms that appeared later, such as Macs with Apple silicon. go.dev/issue/44505 and 54265 record
the related discussion [Cox, 2021] [Cox, 2022]. Go 1.20 raised the
starting point to Go 1.17. From Go 1.22 on, it rises by a fixed rule: building Go 1.N requires Go
1.M, where M is N minus 2, rounded down to an even number. Go releases even-numbered versions in
February and odd-numbered versions in August, so the starting point moves forward once a year and is
always a version released at least a year earlier. The rule is implemented in
requiredBootstrapVersion in cmd/dist [Go Authors, 2026b].
When the version is too old, cmd/dist reports an error as early as possible. Its source directory
contains a file notgo124.go, which takes part in compilation only under versions before Go 1.24
and whose package name is building_Go_requires_Go_1_24_6_or_later. When cmd/dist is compiled
with a Go that is too old, two different package names appear in the same directory, compilation
fails, and the error message contains exactly this sentence. For versions between 1.24 and 1.24.6,
cmd/dist checks the version number and reports the error itself. Any Go newer than required can be
used; the Go 1.27.3 in the experiment above is one example.
3.3.4 Trusting the Compiler
In 1983 Ken Thompson described an attack in his Turing Award lecture “Reflections on Trusting Trust” [Thompson, 1984]. Modify the source of a C compiler so that when it compiles the login program, it quietly plants a backdoor; further make it so that when it compiles the compiler itself, it plants this same modification into the new compiler. Compile the modified source once to get a compiler with the backdoor, then delete the modification from the source. Nothing in the source looks wrong from then on, yet compiling the clean source with this compiler yields a compiler that still has the backdoor, and it passes from generation to generation. Thompson’s conclusion is that code one has not written entirely oneself cannot be fully trusted, and that inspecting the source is not enough.
No self-hosting language can avoid this problem: today’s Go compiler was compiled by the previous Go compiler, which can be traced all the way back to Go 1.4 written in C and to earlier C compilers. In 2005 David A. Wheeler proposed a way to check this, called diverse double-compiling (DDC) [Wheeler, 2005]: choose an independently sourced compiler, use it to compile the source of the compiler under test, use the resulting compiler to compile the same source once more, and compare the result byte by byte with the binary under test. If the two are identical, the binary was indeed produced from this source, unless both starting points were tampered with in exactly the same way.
The experiment at the beginning of this section has exactly this structure: two different starting points yield the same result. But Go 1.26.0 and Go 1.27.3 come from the same project, so the check is not independent. Go’s source can also be built starting from gccgo, an independent implementation in GCC, which is a starting point of truly different origin [Go Authors, 2026a].
Such a check is possible only because builds are reproducible (3.1): once time, paths or
random numbers get mixed into compilation, a byte-by-byte comparison is out of the question.
According to Russ Cox’s 2023 article, Go has had fully reproducible toolchains since Go 1.21: the
toolchain for a given target platform is byte-for-byte identical no matter which operating system it
is built on, and the Go team wrote a tool that rebuilds the official release toolchains from source
and compares them with the published ones [Cox, 2023]. The two builds in the
experiment above are identical to each other, but they cannot be compared byte by byte with the
official downloaded binaries: the official macOS release carries Apple’s code signature (visible
with codesign -dv), while a locally built binary has only the ad-hoc signature added by the
linker.
Further Reading
- [Cox, 2013] Russ Cox. “Go 1.3+ Compiler Overhaul.” 2013. https://go.dev/s/go13compiler https://go.googlesource.com/proposal/+/master/design/go13compiler The plan to translate the compiler written in C into Go automatically, and its rationale.
- [Cox, 2015] Russ Cox. “Go 1.5 Bootstrap Plan.” 2015.
https://go.dev/s/go15bootstrap
https://go.googlesource.com/proposal/+/master/design/go15bootstrap The scheme that removed C
and took an older Go as the starting point; the comments in
make.bashstill point to it. - [Go Authors, 2015] The Go Authors. Go 1.5 Release Notes. https://go.dev/doc/go1.5 The release in which the compiler and runtime were reimplemented in Go.
- [Go Authors, 2026a] The Go Authors. “Installing Go from source.” https://go.dev/doc/install/source The rules for the starting version, and how to start from gccgo.
- [Go Authors, 2026b] The Go Authors.
cmd/dist/build.go,buildtool.go,notgo124.go(Go 1.26). https://github.com/golang/go/tree/release-branch.go1.26/src/cmd/dist https://github.com/golang/go/tree/master/src/cmd/dist The build process of toolchain1 to 3,requiredBootstrapVersionand the version check. - [Cox, 2021] Russ Cox. “build: adopt Go 1.17 as bootstrap toolchain for Go
1.20.” Go issue 44505, 2021. https://go.dev/issue/44505 One of the issues the comment in
notgo124.gopoints to; it discusses why Go no longer starts from Go 1.4. - [Cox, 2022] Russ Cox. “build: adopt Go 1.20 as bootstrap toolchain for Go 1.22.” Go issue 54265, 2022. https://go.dev/issue/54265 Proposes moving the starting point forward every year to the version released the year before.
- [Thompson, 1984] Ken Thompson. “Reflections on Trusting Trust.” Communications of the ACM, 27(8): 761–763, 1984. https://doi.org/10.1145/358198.358210 (ACM Digital Library: https://dl.acm.org/doi/10.1145/358198.358210) The 1983 Turing Award lecture, which describes a compiler backdoor that can pass from generation to generation.
- [Wheeler, 2005] David A. Wheeler. “Countering Trusting Trust through Diverse Double-Compiling.” Proceedings of the 21st Annual Computer Security Applications Conference (ACSAC), 2005. https://dwheeler.com/trusting-trust/ A method of checking a compiler binary with another, independent compiler.
- [Cox, 2023] Russ Cox. “Perfectly Reproducible, Verified Go Toolchains.” The Go Blog, 2023. https://go.dev/blog/rebuild How the Go toolchain became byte-for-byte reproducible, and how official releases are verified independently.
- [GCC, n.d.] GCC Team. Installing GCC: Building. https://gcc.gnu.org/install/build.html GCC’s three-stage bootstrap, and the comparison of the stage 2 and stage 3 compilers.