首页 / 文章 / 运行 go run main.go 时,到底发生了什么?
← 返回
AI技术

运行 go run main.go 时,到底发生了什么?

✍️ zhirenhun 📅 2026/5/30 👁 43 阅读 ⏱ 13 分钟
运行 go run main.go 时,到底发生了什么?

运行 `go run main.go` 时,到底发生了什么?

你可能已经敲了数百次这个命令。

程序成功运行了。

但从你按下 Enter 键到看到第一个 fmt.Println 输出之间,究竟发生了什么?

完整流程图

image

步骤 0:你按下 Enter

你的 shell 在 $PATH 中找到了 go 命令,通常路径是 /usr/local/go/bin/go,或者由你的版本管理器(如 asdfgoenv)安装在的任何位置。

npm run dev 不同,这里没有一个单独的启动器脚本。

go 二进制文件是整个工具链(toolchain)的单一前端,它集成了构建驱动(build driver)、依赖管理器、测试运行器等所有功能。

步骤 1:`go` 命令解析你的参数

go run 是众多子命令(如 go buildgo testgo mod tidy 等)之一。

go 二进制文件会根据传入的参数进行分派。

对于 go run main.go,它本质上执行了两个动作:构建 (build),然后 执行 (exec) 结果。

需要澄清的一点是:go 命令本身并不负责编译任何东西。

它是一个编排器(driver),负责协调整个构建过程,它会调用位于 $GOROOT/pkg/tool/$GOOS_$GOARCH/ 目录下的各个独立工具程序——包括 compileasmlinkpack——每个工具都是一个独立的进程。

你可以使用 go build -x 来观察整个序列,它会打印出它调用的每一个工具。

lovestaco@i3nux-mint:~/pers/blogs/go-rate-limiters$ go build -x 01_token_bucket_basic.go
WORK=/tmp/go-build1951611363
mkdir -p $WORK/b001/
cat >/tmp/go-build1951611363/b001/importcfg << 'EOF' # internal
# import config
packagefile context=/home/lovestaco/.cache/go-build/15/1575351e64b96edf357d10ac88f3cab30fcb1cfa9a191331bcad6190a73853f4-d
packagefile fmt=/home/lovestaco/.cache/go-build/51/516c0f36e371fc103e5e31bffc64e8899c256a1d9278377baeaee3c3d67d5990-d
packagefile golang.org/x/time/rate=/home/lovestaco/.cache/go-build/d5/d5dd928826b919bdb04e4142718ffda25ffa97a1f788e61444596c150f6df76a-d
packagefile time=/home/lovestaco/.cache/go-build/2b/2ba963660e1b7abf46deaef03614f8cce1f6230e971041e2f60a9b8b5bca5326-d
packagefile runtime=/home/lovestaco/.cache/go-build/f0/f09fa4ced4d67a86c477e2daecfaf39fccd7d0cb936478c5d73427221189029e-d
EOF
cd /home/lovestaco/pers/blogs/go-rate-limiters
/usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -lang=go1.24 -complete -buildid cR1ay72gpouW8Dj1wpHA/cR1ay72gpouW8Dj1wpHA -goversion go1.24.4 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./01_token_bucket_basic.go
/usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /home/lovestaco/.cache/go-build/7f/7fa4c76fda90e3f91bee04d9326171d548b473efc9a5a49059e5650d52c7ed40-d # internal
cat >/tmp/go-build1951611363/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=/tmp/go-build1951611363/b001/_pkg_.a
packagefile context=/home/lovestaco/.cache/go-build/15/1575351e64b96edf357d10ac88f3cab30fcb1cfa9a191331bcad6190a73853f4-d
packagefile internal/oserror=/home/lovestaco/.cache/go-build/38/38a0285b4078334a8d1c6a928e079b3ee65331a955fae62da547452857e18da5-d
packagefile path=/home/lovestaco/.cache/go-build/24/24092a25546a3b8c3771592daca75c8fb2d416c84952f466cc92fcb99959688c-d
modinfo "0wxaffx92tx02Axe1xc1axe6xd6x18xe6path	command-line-arguments
dep	golang.org/x/time	v0.5.0	h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
build	-buildmode=exe
build	-compiler=gc
build	DefaultGODEBUG=asynctimerchan=1,gotestjsonbuildtext=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,multipathtcp=0,randseednop=0,rsa1024min=0,tls10server=1,tls3des=1,tlsmlkem=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1,x509rsacrt=0,x509usepolicies=0
build	CGO_ENABLED=1
build	CGO_CFLAGS=
build	CGO_CPPFLAGS=
build	CGO_CXXFLAGS=
build	CGO_LDFLAGS=
build	GOARCH=amd64
build	GOOS=linux
build	GOAMD64=v1
xf92C1x86x18 rx00x82Bx10Ax16xd8xf2"
EOF
mkdir -p $WORK/b001/exe/
cd .
GOROOT='/usr/local/go' /usr/local/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -X=runtime.godebugDefault=asynctimerchan=1,gotestjsonbuildtext=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,multipathtcp=0,randseednop=0,rsa1024min=0,tls10server=1,tls3des=1,tlsmlkem=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1,x509rsacrt=0,x509usepolicies=0 -buildmode=exe -buildid=wvZLsb-w6x3yjmaurfAY/cR1ay72gpouW8Dj1wpHA/ipt4_u-REyDFqYjqF2h5/wvZLsb-w6x3yjmaurfAY -extld=gcc $WORK/b001/_pkg_.a
/usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
cp $WORK/b001/exe/a.out 01_token_bucket_basic
rm -rf $WORK/b001/
```

步骤 2:包加载 — 读取导入图 (Import Graph)

在任何一行代码被编译之前,Go 都需要知道它要编译“什么”。

它会读取你的 go.mod 文件来确定模块的根目录和依赖的版本。

接着,它会递归地遍历你的导入(imports),构建一个有向无环图(DAG)来表示各个 package 之间的依赖关系。

标准库的 package 源(如 fmtosnet/http)位于 $GOROOT/src

你自己的 package 是相对于模块根目录解析的。

第三方 package 则来自模块缓存(module cache)中的 $GOPATH/pkg/mod

这个图是所有其他操作的基础。

步骤 3:编译流水线 (Compilation Pipeline)

这正是 Go 获得其速度声誉的地方。

导入图中每一个 package 都会独立地经过以下流水线:

image

Lexer (词法分析器) → 将原始的源代码字符转换成一个扁平化的 token 流(如 funcmain() 等)。

Parser (解析器) → 从这些 token 中构建一个抽象语法树 (Abstract Syntax Tree, AST)。Go 的 parser 是手工编写的(而非自动生成的),并且设计得非常简洁,其语言语法足够小,可以被人类轻松掌握。

Type checker (类型检查器) → 遍历 AST,验证每一个表达式的类型是否正确。同时,这也是 Go 解析标识符和构建其作用域链(scope chain)的地方。

SSA → 优化的 IR 随后被降级为静态单赋值(Static Single Assignment, SSA)形式,这是一种更底层的表示形式。编译器在此形式下会运行死代码消除(dead-code elimination)和常见子表达式消除(common-subexpression elimination)等优化过程,随后进行特定于机器的降级(lowering),为代码生成(codegen)做准备。

Codegen → SSA 被转换为原生的机器代码,以每个包的目标文件(object file)形式写入,并被打包进一个 .a 归档文件(archive)中,供链接器(linker)后续使用。

使此过程快速的有两个关键因素:

第 4 步:链接器(The linker)

一旦所有 .a 文件都存在,链接器就会将它们组合成一个单一的、自包含的二进制文件。

对于 go run 命令,这个二进制文件不会放在你的工作目录中,而是会落入一个临时工作目录(类似于 $TMPDIR/go-buildXXXXXXXX/),然后被执行(exec'd)。

Go 的链接器会将整个 Go 运行时(Go runtime)包含在每个二进制文件中。

你无需担心共享的运行时 DLL。

这就是为什么纯 Go(pure-Go)二进制文件默认是静态链接(statically linked)的,这也是为什么你可以将它 scp 到任何兼容的机器上,它就能直接运行,无需安装依赖步骤。(需要注意的例外情况是:使用 cgo 的程序,或者某些可能引入系统 C 解析器的标准库包,如 netos/user,除非你使用 CGO_ENABLED=0 构建,否则它们可能会动态链接到 libc。)

第 5 步:运行时初始化 — 在你的代码运行之前

这是大多数 Go 开发者从未深思过的地方。当操作系统加载你的二进制文件时,第一个运行的不是 main()

而是 Go 运行时的引导程序(bootstrap),它是用汇编语言编写的(例如 rt0_amd64.s 或等效文件)。

image

栈和 TLS 设置 — 运行时会分配初始的 goroutine 栈(从 8KB 开始,动态增长),并设置线程本地存储(thread-local storage, TLS),以便每个操作系统线程知道它正在运行哪个 goroutine。

M:P:G 调度器 — Go 的并发模型涉及三个抽象层。G(goroutines)是轻量级的绿色线程。

M(machine)是实际运行 goroutine 的操作系统线程。

P(processor)是一个带有运行队列的逻辑处理器;其数量由 GOMAXPROCS 决定。默认情况下,它等于逻辑 CPU 的数量——尽管自 Go 1.25 以来,在 Linux 上,如果进程在 cgroup CPU 限制下运行(如大多数容器所示),而该限制低于核心数,GOMAXPROCS 将默认使用该限制,并且运行时会定期重新检查它。

所有这些基础设施在到达 main() 之前都会启动。

垃圾回收器(Garbage collector, GC) — GC 的数据结构被初始化。

init() 函数 — 每个包都可以定义 init() 函数。

运行时会按照依赖顺序调用它们:一个包的 init() 只有在其所有导入包的 init() 函数都完成后才会运行。

你的 main 包的 init() 是最后一个运行的。

只有那时,运行时才会调用 main.main()

第 6 步:`main()` 运行

你的代码终于在主 goroutine 上执行了。

一个微妙的点是:如果 main() 返回,进程就会退出——即使其他 goroutine 仍在运行。

主 goroutine 是特殊的;它不会返回到线程池中。

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

← 上一篇
MongoDB $facet 详解
下一篇 →
为什么 AI 会忘记你说过的话(以及如何解决)

📌 相关推荐

走向 Agent 记忆的标准模型
2026/5/31
浏览器内部的悄然 AI 战争
2026/5/31
为什么 AI 会忘记你说过的话(以及如何解决)
2026/5/31
← 返回文章列表