1.Java VS Go语言
Java,从源代码到编译成可运行的代码
上图已经展示了这个过程:从Java的源代码编译成jar包或war包(字节码),最终运行在JVM中。
我们把Java源代码编译后的jar包或war包看成是工程师生产出来的产品,操作系统是一个平台,JVM就是中间商,那程序的整体性能也要受到中间商JVM的因素影响了。
- 优点:一次编译,到处运行(windows、linux、macos)
- 缺点:JVM性能损失大。
Go语言,从源代码到编译成可运行的代码
我们把Go语言的源代码编译后,生成二进制文件,直接就可以在操作系统上运行,没有中间商。
优点:
- 直接编译成二进制
- 无需进行虚拟机环境,自动执行
- 一次编写代码,跨平台执行
- 高性能并发能力
2.为什么Go语言运行-“没有中间商”
每种编程语言都有自己的Runtime, 把这个单词拆开来看,Run=运行,Time=时间,简称:运行时。
Go语言的Runtime作用:
- 内存管理
- 协程调度
- 垃圾回收
Go语言的运行时,是和源代码最终编译生成到二进制文件中的。当我们启动二进制文件的时候,运行时也就是一并启动了。
Go语言是如何编译成二进制文件的
package main
import "fmt"
func main() {
fmt.Println("面向加薪学习-从0到Go语言微服务架构师")
}
在命令行执行 go build -n(-n含义代表:打印编译时会用到的所有命令,但不真正执行)
编译过程1
从上图可以看到:
- import config 导入配置
- fmt.a—>对应fmt包
- runtime.a—>对应runtime包
- compile -o 编译输出到 pkg.a
编译过程2
- 创建exe目录
- link链接到a.out
- 把a.out该名成menu1
总结:看到上面的过程已经把runtime包放到我们的二进制文件中了。
3.编译过程
在编译原理中,有一个名词:AST(抽象语法树) = Abstract Syntax Tree
1. 把源代码变成文本,然后把每个单词拆分出来
2. 把每个单词变成语法树
3. 类型检查、类型推断、类型匹配、函数调用、逃逸分析
分析阶段
- 词法检查分析、语法检查分析、语义检查分析)
- 生成中间码生成(SSA代码,类似汇编)。
执行export GOSSAFUNC=main,代表你要看main函数的ssa代码,然后执行go build,会生成ssa.html
图1. - 图2.
- 代码优化
- 生成机器码(支持生成.a的文件)
- go build -gcflags -S main.go(生成和平台相关的plan9汇编代码)
- 链接(生成可执行二进制文件)
4.Go语言是如何启动的
Go语言启动的时候,Runtime到底发生了什么?
可以到runtime目录中找到rt0_darwin_amd64.s找到这个文件(由于我的电脑是mac,所以找到了这个,其他平台可以找各自的),这是一个汇编文件。
rt0_darwin_amd64.s
TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
asm_amd64.s
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
接下来在同名文件中找到
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
它执行
- 在堆栈上复制参数。
- 从给定的(操作系统)堆栈中创建 iStack。
- _cgo_init(可能会更新堆栈保护)
- 收集用到的处理器信息
上面信息就是初始化一个协程G0(这是一个根协程,此时还没有调度器,也就是说不受调度器控制)
接下来是各种平台的检测和判断
CALL runtime·check(SB)
查找代码 在runtime1.go,很亲切的Go语言函数了吧。里面是各种检查。看看都干了啥。
func check() {
unsafe.Sizeof(...)
unsafe.Offsetof(...)
atomic.Cas(...)
atomic.Or8()
unsafe.Pointer()
if _FixedStack != round2(_FixedStack){
...
}
...
}
上面代码执行了:
- 检查类型长度是否合法
- 检查偏移量是否合法
- 检查CAS执行是否合法
- 检查原子执行是否合法
- 检查指针执行是否合法
- 判断栈大小是否是2的幂次方
接下来
CALL runtime·args(SB)
func args(c int32, v **byte) {
argc = c
argv = v
sysargs(c, v)
}
下面看一下启动顺序:
osinit(操作系统的初始化) -> schedinit(调度器的初始化) -> make & queue new G(新建一个队列G) -> mstart(启动)
CALL runtime·osinit(SB)
func osinit() {
ncpu = getncpu()
physPageSize = getPageSize()
}
runtime/proc.go
CALL runtime·schedinit(SB)
func schedinit() {
...
stackinit() //栈空间内存分配
mallocinit() //堆内存空间初始化
cpuinit() // must run before alginit
alginit() // maps, hash, fastrand must not be used before this call
fastrandinit() // must run before mcommoninit
mcommoninit(_g_.m, -1)
modulesinit() // provides activeModules
typelinksinit() // uses maps, activeModules
itabsinit() // uses activeModules
stkobjinit() // must run before GC starts
...
goargs()
goenvs()
parsedebugvars()
gcinit()
}
可以看到上面的代码的操作:
- CPU初始化
- 栈空间初始化
- 堆空间初始化
- 命令行参数初始化
- 环境变量初始化
- GC初始化
//拿到主函数的地址 ,是$runtime·main的地址,这里还没到我们写的main函数呢
MOVQ $runtime·mainPC(SB), AX
PUSHQ AX
//启动一个新协程
CALL runtime·newproc(SB)
POPQ AX
//启动一个M(可以把M看成是一个中间人,它联系Goroutine和Processor)
CALL runtime·mstart(SB)
从上面看到,此时系统里拥有:
- G0-根协程
- runtime.main的主协程
- 启动了M等待调度
runtime.main在runtime/proc.go中(这个是runtime中的main方法,还没到我们自己写的main函数)
// The main goroutine.
func main() {
g := getg()
...
doInit(&runtime_inittask)
gcenable()
fn := main_main
fn()
}
从上面看到:
- getg() 获取当前的goroutine
- 对g做判断和设置操作
- 初始化runtime doInit(…)
- 启用GC
- fn := main_main 这是隐式的调用,因为linker运行时不知道主包的地址。在之前的学习,我们知道编译过程有链接的时候,就会从main_main去找main.main。这个时候,才真正执行到我们程序员写的代码中。 go:linkname main_main main.main