写点什么

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

  • 2022-12-05
    北京
  • 本文字数:2402 字

    阅读完需:约 8 分钟

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

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



从上图可以看到:


  1. import config 导入配置

  2. fmt.a--->对应 fmt 包

  3. runtime.a--->对应 runtime 包

  4. compile -o 编译输出到 pkg.a


编译过程 2



  1. 创建 exe 目录

  2. link 链接到 a.out

  3. 把 a.out 该名成 menu1


总结:看到上面的过程已经把 runtime 包放到我们的二进制文件中了。

3.编译过程

在编译原理中,有一个名词:AST(抽象语法树) = Abstract Syntax Tree1. 把源代码变成文本,然后把每个单词拆分出来 2. 把每个单词变成语法树 3. 类型检查、类型推断、类型匹配、函数调用、逃逸分析

分析阶段

  1. 词法检查分析、语法检查分析、语义检查分析)

  2. 生成中间码生成(SSA 代码,类似汇编)。 执行export GOSSAFUNC=main,代表你要看main函数的ssa代码,然后执行go build,会生成ssa.html图 1.

  3. 图 2.

  4. 代码优化

  5. 生成机器码(支持生成.a 的文件)

  6. go build -gcflags -S main.go(生成和平台相关的 plan9 汇编代码)

  7. 链接(生成可执行二进制文件)

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 函数)


// 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

发布于: 刚刚阅读数: 7
用户头像

还未添加个人签名 2018-04-27 加入

已出版《Go语言极简一本通》 B站ID:面向加薪学习,公众号:面向加薪学习 加微信write_code_666,进群,可以和更多小伙伴交流。

评论

发布
暂无评论
底层逻辑-理解Go语言的本质_golang_面向加薪学习_InfoQ写作社区