在本地机器编译 Golang 源码
研究一门编程语言的实现原理,第一步就是要在自己的计算机上,通过源码成功编译出适应本系统的安装包(也叫源码安装)。
一言以蔽之
在 M2 芯片的 Mac 上编译最新的 Golang。
环境信息
- 目标 Golang 源码分支:
master
- 操作系统:macOS
- 芯片架构:Apple M2 Pro
为什么要从源码编译
在研究语言实现过程中,我们不可避免要进行 Debug、打印日志甚至修改某些实现逻辑,如果直接使用编译好的 Golang 环境,源码不可修改。
自举
在开始编译我们自己的 Go 版本之前,先来看一个问题:我们日常开发 Go 程序,只需要编写一个 xxx.go
文件,然后使用 Go 提供的编译命令 go build xxx.go
就可以编译出本平台的可执行文件进而运行。
之所以能愉快的使用 go
命令,本质上是我们在本地电脑安装了 Go 的包(环境),执行 go build
命令实际上是调用了 Go 的编译器将我们的代码编译成了可执行文件。
那么思考这个问题:Golang 的编译器是由什么语言实现的呢?查看 Github 上 Go 的开源仓库我们知道,目前的 Go 编译器,是 Go 实现的。
Go 自己编译自己?很明显这陷入了 “先有鸡还是先有蛋” 的问题。不过这个问题在这里有明确的答案 —— 自举。
什么是自举?通俗来讲,实现编程语言的自举,就是能使用该语言本身编写的编译器或解释器来编译或运行该语言程序的过程。以 Go 为例,自举的大致过程如下:
- 实现前端编译器:使用 C 语言实现并编译 Go 的基础编译器(gc,go compiler,和垃圾收集的 GC 不是一回事),功能相对有限。
- 自举编译器:使用 Go 重写功能相对完整的 Go 编译器,并使用 gc 来编译。
- 使用新的 Go 编译器,编译自身代码,确保它可以生成与 gc 编译结果相同的结果,至此自举成功。
Golang 从 1.5 版本开始实现了自举。这对语言的发展意义重大。意味着 Golang 的语言功能已经基本完善,具备了可验证的图灵完备性。
源码编译
你当然可以先编译出 1.4 版本的编译器(基于 clang,不依赖已有的 Go 环境),再用其编译新的 Go 版本。
但因为 Golang 已经实现了自举,因此我们编译 Golang 源码,就直接使用 Go 实现的编译器即可,而且编译器也是不断迭代优化的。
获取最新源码
cd ~/Documents/doers
git clone https://github.com/golang/go.git
cd go/src
查看本地已有 Go 环境
go env
# 输出
GOPATH='~/go'
GOROOT='/usr/local/go/bin'
可以看到笔者本地存在 Go 环境。稍后我们将使用这个环境(的编译器)来编译最新的 Go 源码。
修改 Go 源码实现
我们对最简单的 fmt
包下的 Println
函数做简单的修改,来验证我们的编译是否成功和生效。
package fmt
...
func Println(a ...any) (n int, err error) {
println("你好呀 🎉")
return Fprintln(os.Stdout, a...)
}
编译源码
Go 源码中已经提供了编译脚本,我们直接使用即可。
tips
: 从脚本内容我们知道,这里要注意一点:要确保在 src
目录下执行 make.bash
,否则会报错:make.bash must be run from $GOROOT/src
。
pwd
~/Documents/doers/go/src
# 执行编译脚本
./make.bash
# 输出
Building Go cmd/dist using /usr/local/go. (go1.22.5 darwin/arm64)
Building Go toolchain1 using /usr/local/go.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for darwin/arm64.
---
Installed Go for darwin/arm64 in ~/Documents/doers/go
Installed commands in ~/Documents/doers/go/bin
当看到输出中有 Installed ...
信息时,就代表我们编译成功了 🎉,这里我们解读下上述过程:
make.bash
脚本只执行安装过程(不包含测试标准库的过程)。Building Go cmd/dist using /usr/local/go. (go1.22.5 darwin/arm64)
代表使用了笔者本地GOROOT
路径下的 Go 命令来构建出cmd/dist/dist
。
GOROOT_BOOTSTRAP_VERSION=$(bootstrapenv "$GOROOT_BOOTSTRAP/bin/go" version | sed 's/go version //')
echo "Building Go cmd/dist using $GOROOT_BOOTSTRAP. ($GOROOT_BOOTSTRAP_VERSION)"
if $verbose; then
echo cmd/dist
fi
if [[ "$GOROOT_BOOTSTRAP" == "$GOROOT" ]]; then
echo "ERROR: $GOROOT_BOOTSTRAP must not be set to $GOROOT" >&2
echo "Set $GOROOT_BOOTSTRAP to a working Go tree >= Go $bootgo." >&2
exit 1
fi
rm -f cmd/dist/dist
bootstrapenv "$GOROOT_BOOTSTRAP/bin/go" build -o cmd/dist/dist ./cmd/dist
# -e doesn't propagate out of eval, so check success by hand.
eval $(./cmd/dist/dist env -p || echo FAIL=true)
if [[ "$FAIL" == true ]]; then
exit 1
fi
if $verbose; then
echo
fi
if [[ "$1" == "--dist-tool" ]]; then
# Stop after building dist tool.
mkdir -p "$GOTOOLDIR"
if [[ "$2" != "" ]]; then
cp cmd/dist/dist "$2"
fi
mv cmd/dist/dist "$GOTOOLDIR"/dist
exit 0
fi
# 运行 ./cmd/dist/dist bootstrap 开始安装
# Run dist bootstrap to complete make.bash.
# Bootstrap installs a proper cmd/dist, built with the new toolchain.
# Throw ours, built with the bootstrap toolchain, away after bootstrap.
./cmd/dist/dist bootstrap -a $vflag $GO_DISTFLAGS "$@"
rm -f ./cmd/dist/dist
这是一个引导程序,调用 dist bootstrap
可以完成整个 Go 源码的编译。接下来的步骤就进入了 dist
的工作范围。
Building Go toolchain1 using /usr/local/go
代表使用本地GOROOT
路径下的 Go 命令来构建出toolchain1
,它是新的编译工具链。toolchain2
和toolchain3
其实是依次重新编译出的新工具链,这里涉及到 Go 构建的可重现性问题,限于篇幅就不再赘述。Russ Cox 对此过程有一篇博客说明:Perfectly Reproducible, Verified Go Toolchains - The Go Programming Language,感兴趣可以阅读下。值得一提的是,dist bootstrap
引导过程部分的代码作者正是 Russ Cox。- 最终,执行程序会使用 go_bootstrap,编译出完整的 go 可执行程序。
至此,我们成功的在本地环境构建了自己的 Go!🎉,查看下版本:
pwd
~/Documents/doers/go
./bin/go version
# 输出结果
go version devel go1.24-b8f83e2270 Mon Jul 22 21:51:21 2024 +0000 darwin/arm64
验证结果
别忘记,我们曾经修改过 Println
函数的实现,是时候验证下了。
验证的过程很简单,我们在当前终端会话中设置临时的环境变量:
pwd
~/Documents/doers/go
export GOROOT=~/Documents/doers/go
export GOPATH=~/Documents/doers/go/local
export GOBIN=${GOPATH}/bin
export PATH=${PATH}:${GOBIN}:${GOROOT}/bin
然后在 GOPATH
下编写测试代码:
package main
import "fmt"
func main() {
fmt.Println("hello go")
}
使用我们编译好的 go
命令运行它:
pwd
~/Documents/doers/go
# 运行 main.go
./bin/go run ./local/src/main.go
执行后最终输出结果为:
你好呀 🎉
hello go
至此,我们完成了整个本地编译 Go 的过程,为后续深入探究 Go 源码设计和语言实现做好了准备 ✌️。
- 更多 Go 高质量内容
https://portal.yunosphere.com
- 欢迎关注,分享 Go / 创业经历 / 其他编程知识 👇
本作品采用《CC 协议》,转载必须注明作者和本文链接