底层逻辑-理解Go语言的本质

i-coder-robot · · 2564 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

# 1.Java VS Go语言 ### Java,从源代码到编译成可运行的代码 <img src="https://tva1.sinaimg.cn/large/008vxvgGly1h8n0n13u65j30kw070q37.jpg" alt="java组图1" style="zoom:50%;" /> **上图已经展示了这个过程:从Java的源代码编译成jar包或war包(字节码),最终运行在JVM中。** <img src="https://tva1.sinaimg.cn/large/008vxvgGly1h8n2garvtrj31860qkq4s.jpg" alt="java组图2" style="zoom: 33%;" /> **我们把Java源代码编译后的jar包或war包看成是工程师生产出来的产品,操作系统是一个平台,JVM就是中间商,那程序的整体性能也要受到中间商JVM的因素影响了。** - **优点**:一次编译,到处运行(windows、linux、macos) - **缺点**:JVM性能损失大。 ### Go语言,从源代码到编译成可运行的代码 <img src="https://tva1.sinaimg.cn/large/008vxvgGly1h8n3f58h2bj315a0u0gnn.jpg" alt="golang组图1" style="zoom: 33%;" /> **我们把Go语言的源代码编译后,生成二进制文件,直接就可以在操作系统上运行,没有中间商**。 **优点:** - **直接编译成二进制** - **无需进行虚拟机环境,自动执行** - **一次编写代码,跨平台执行** - **高性能并发能力** # 2.为什么Go语言运行-"没有中间商" 每种编程语言都有自己的Runtime, 把这个单词拆开来看,Run=运行,Time=时间,简称:**运行时**。 Go语言的Runtime作用: - **内存管理** - **协程调度** - **垃圾回收** **Go语言的运行时,是和源代码最终编译生成到二进制文件中的。当我们启动二进制文件的时候,运行时也就是一并启动了。** ### Go语言是如何编译成二进制文件的 ```go package main import "fmt" func main() { fmt.Println("面向加薪学习-从0到Go语言微服务架构师") } ``` **在命令行执行 go build -n(-n含义代表:打印编译时会用到的所有命令,但不真正执行)** **编译过程1** <img src="https://tva1.sinaimg.cn/large/008vxvgGly1h8n5jzkib8j319k0u0gq3.jpg" alt="编译过程1" style="zoom: 33%;" /> **从上图可以看到:** 1. **import config 导入配置** 2. **fmt.a--->对应fmt包** 3. **runtime.a--->对应runtime包** 4. **compile -o 编译输出到 _pkg_.a** **编译过程2** <img src="https://tva1.sinaimg.cn/large/008vxvgGly1h8n600cjfrj32lu0hqn33.jpg" alt="编译过程2" style="zoom: 33%;" /> 1. **创建exe目录** 2. **link链接到a.out** 3. **把a.out该名成menu1** **总结:看到上面的过程已经把runtime包放到我们的二进制文件中了。** # 3.编译过程 **在编译原理中,有一个名词:AST(抽象语法树) = Abstract Syntax Tree** **1. 把源代码变成文本,然后把每个单词拆分出来** **2. 把每个单词变成语法树** **3. 类型检查、类型推断、类型匹配、函数调用、逃逸分析** ## 分析阶段 1. **词法检查分析、语法检查分析、语义检查分析)** 2. **生成中间码生成(SSA代码,类似汇编)。** ``` 执行export GOSSAFUNC=main,代表你要看main函数的ssa代码,然后执行go build,会生成ssa.html``` <br/><br/> 图1. <img src="https://tva1.sinaimg.cn/large/008vxvgGly1h8n9qsqpduj31k70u0k3e.jpg" alt="ssa1" style="zoom: 33%;" /> <br/><br/> 图2. <img src="https://tva1.sinaimg.cn/large/008vxvgGly1h8n9r7vr1xj30ti0emtc0.jpg" alt="ssa2" style="zoom: 33%;" /> 3. **代码优化** 4. **生成机器码(支持生成.a的文件)** 5. **go build -gcflags -S main.go(生成和平台相关的plan9汇编代码)** 6. **链接(生成可执行二进制文件)** # 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() } ``` **可以看到上面的代码的操作:** 1. **CPU初始化** 2. **栈空间初始化** 3. **堆空间初始化** 4. **命令行参数初始化** 5. **环境变量初始化** 6. **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) ``` 从上面看到,此时系统里拥有: 1. **G0-根协程** 2. **runtime.main的主协程** 3. **启动了M等待调度** runtime.main在runtime/proc.go中(这个是runtime中的main方法,还没到我们自己写的main函数) ```go // The main goroutine. func main() { g := getg() ... doInit(&runtime_inittask) gcenable() fn := main_main fn() } ``` **从上面看到:** 1. getg() 获取当前的goroutine 2. 对g做判断和设置操作 3. 初始化runtime doInit(...) 4. 启用GC 5. fn := main_main 这是隐式的调用,因为linker运行时不知道主包的地址。在之前的学习,我们知道编译过程有链接的时候,就会从main_main去找main.main。这个时候,才真正执行到我们程序员写的代码中。 go:linkname main_main main.main #### 1元购买《Go语言+支付宝》实战课 <img src="https://tva1.sinaimg.cn/large/008vxvgGgy1h8uc6ofy44j30zo0u0af4.jpg" alt="Go+支付宝实战课" style="zoom: 33%;" /> #### Go语言微服务学习路线图 <img src="https://tva1.sinaimg.cn/large/008vxvgGgy1h8ucdlqdkfj30w90u0teh.jpg" alt="Go语言微服务学习路线图" style="zoom: 33%;" /> | <font color="#00C4B6" size="4px">添加微信</font> | <font color="#00C4B6" size="4px">公众号更多内容</font> | | ------------------------------------------------------------------ | --------------------------------------------------------------- | | ![wechat](https://i.postimg.cc/XYFRKsdy/wechat.png) | ![gzh](https://tva1.sinaimg.cn/large/008vxvgGgy1h8ucjtghyej304s04sglo.jpg) |

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2564 次点击  
加入收藏 微博
1 回复  |  直到 2022-12-07 09:34:32
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传