1. Go语言是如何编译的
1. Go 程序是如何编译的
1.1 什么是 runtime?
runtime 就是程序的运行环境
- java:java 虚拟机
- javaStript:浏览器内核
Go 语言的 runtime:
- Go 语言没有虚拟机这个概念
- Go 的 runtime 作为程序的一部分被打包到二进制文件中
- runtime 也会随用户程序一起运行
- runtime 与用户程序没有明显的界限,直接通过函数调用

runtime 的能力:
- 内存管理能力
- 垃圾回收能力(GC)
- 超强的并发能力(协程调度)
runtime 的其他特点:
- runtime 有一定的屏蔽系统调用能力
- 一些 Go 的关键字其实是 runtime 下的函数

总结:
- Go 的 Runtime 负责内存管理、垃圾回收、协程调度
- G o的 Runtime 被编译为用户程序的一部分,一起运行
1.2 Go 是如何编译的?
例如如下程序:
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
- 使用 go build -n 命令查看编译过程:



1.3 Go 的编译过程

- 词法分析:将源代码翻译成 Token,token 是代码中的最小语义结构
- 句法分析:将 token 处理后编程语法树如下:
-
语义分析:类型检查、类型推断、查看类型是否匹配,函数调用内联、逃逸分析
-
中间码生成(SSA):为了处理不同平台的差异,先生成中间代码
- 查看中间代码的整个过程
$env:GOOSFUNC="main" # windows # export GOOSAFUNC=main # linux go build -
代码优化:每一步其实都可能进行代码优化
-
机器码生成:
- 先生成 Plan9 汇编代码(查看 Plan9 汇编代码:
go build -gcflags -S main.go) - 最后编译为机器码
- 输出的机器码为 .a 文件
![]()
- 先生成 Plan9 汇编代码(查看 Plan9 汇编代码:
-
链接:将各个包进行链接,包括 runtime
总结:

1.4 Go 程序运行过程
Go 程序的入口是什么?
Go 语言的程序入口其实在 runtime 包下,该包下有很多 rt_..._...s 文件,如下:

- rt0:runtime,0 表示 runtime 的入口
- linux:表示操作系统
- amd64:芯片架构
Go 语言运行的时候,就会先运行电脑对应的方法
我们可以查看 _rt0_amd64(SB) 方法,在 \runtime\asm_amd64.s 文件中:

- 该方法将 argc 和 argv 存放到寄存器中
- argc 和 argv 表示调用命令时跟的参数,例如使用 go build -n,-n 就是参数
- 调用 runtime.rt0_go(SB)
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
// copy arguments forward on an even stack
MOVQ DI, AX // argc
MOVQ SI, BX // argv
SUBQ $(4*8+7), SP // 2args 2auto
ANDQ $~15, SP
MOVQ AX, 16(SP)
MOVQ BX, 24(SP) // 1. 将参数放到栈上
// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
// 2. 初始化一个叫 g0 的协程,g0 是为了调度协程而产生的协程,g0 是go程序的第一个协程
MOVQ $runtime·g0(SB), DI
LEAQ (-64*1024+104)(SP), BX
MOVQ BX, g_stackguard0(DI)
MOVQ BX, g_stackguard1(DI)
MOVQ BX, (g_stack+stack_lo)(DI)
MOVQ SP, (g_stack+stack_hi)(DI)
...
ok:
// set the per-goroutine and per-mach "registers"
get_tls(BX)
LEAQ runtime·g0(SB), CX
MOVQ CX, g(BX)
LEAQ runtime·m0(SB), AX
// save m->g0 = g0
MOVQ CX, m_g0(AX)
// save m0 to g0->m
MOVQ AX, g_m(CX)
CLD // convention is D is always left cleared
CALL runtime·check(SB) // 3. 运行时检测,这是一个go语言的方法,在runtime1.go中(检查各类型的长度,指针操作、结构体字段的偏移量,atomic原子操作,CAS操作,栈大小是否是2的次幂)
// 4. 将 argc 和 argv 拷贝到 go 代码中,参数的数量赋值给 argc int32,参数的值赋给 argv **byte
MOVL 16(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 24(SP), AX // copy argv
MOVQ AX, 8(SP)
CALL runtime·args(SB)
CALL runtime·osinit(SB) // 5. 判断系统的字长和CPU的核数
CALL runtime·schedinit(SB) // 6. 调度器初始化(全局栈空间内存分配、加载命令行参数到os.Args、堆内存空间的初始化、加载操作系统环境变量、初始化当前系统线程、垃圾回收期的参数初始化、算法初始化map/hash、设置process数量)
// create a new goroutine to start program
// 7. 取出runtime.main方法的地址,并不是我们自己写的main方法的地址
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
PUSHQ $0 // arg size
CALL runtime·newproc(SB) // 8. 启动一个新协程
POPQ AX
POPQ AX
// start this M
CALL runtime·mstart(SB) // 9. 初始化调度器的M,我们新建的协程就会真正的执行
CALL runtime·abort(SB) // mstart should never return
RET
// Prevent dead-code elimination of debugCallV2, which is
// intended to be called by debuggers.
MOVQ $runtime·debugCallV2<ABIInternal>(SB), AX
RET
目前为止,我们的系统已经开启了两个协程(g0 和运行 runtime.main 的协程->主协程),将 runtime.main 放入调度器等待调度
初始化 M,用来调度主协程
主协程执行执行主函数
查看 runtime.main,在 runtime/proc.go 中
// The main goroutine.
func main() {
doInit(&runtime_inittask) // Must be before defer. 1. 执行 runtime 包中的 init 方法
// Defer unlock so that runtime.Goexit during init does the unlock too.
needUnlock := true
defer func() {
if needUnlock {
unlockOSThread()
}
}()
gcenable() // 2. 启动垃圾回收
...
doInit(&main_inittask) // 3. 执行用户包依赖的 init 方法
...
// 4. 这里就是调用我们自己的main方法
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
...
}
- 查看 main_main
//go:linkname main_main main.main
func main_main()
- 链接到我们自己的 main 方法
总结:主协程执行主函数
- 执行 runtime 包中的 init 方法
- 启动 GC 垃圾收集器
- 执行用户包依赖的 init 方法
- 执行用户主函数 main.main()


浙公网安备 33010602011771号