6

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

 1 year ago
source link: https://studygolang.com/articles/35988
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1.Java VS Go语言

Java,从源代码到编译成可运行的代码

java组图1

上图已经展示了这个过程:从Java的源代码编译成jar包或war包(字节码),最终运行在JVM中。

java组图2

我们把Java源代码编译后的jar包或war包看成是工程师生产出来的产品,操作系统是一个平台,JVM就是中间商,那程序的整体性能也要受到中间商JVM的因素影响了。

  • 优点:一次编译,到处运行(windows、linux、macos)
  • 缺点:JVM性能损失大。

Go语言,从源代码到编译成可运行的代码

golang组图1

我们把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

从上图可以看到:

  1. import config 导入配置
  2. fmt.a--->对应fmt包
  3. runtime.a--->对应runtime包
  4. compile -o 编译输出到 pkg.a

编译过程2

编译过程2
  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

    图1.
    ssa1
    图2.
    ssa2
  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函数)

// The main goroutine.
func main() {
    g := getg()
    ...
    doInit(&runtime_inittask)
    gcenable()
    fn := main_main 
    fn()
}

从上面看到:

  1. getg() 获取当前的goroutine
  2. 对g做判断和设置操作
  3. 初始化runtime doInit(...)
  4. fn := main_main 这是隐式的调用,因为linker运行时不知道主包的地址。在之前的学习,我们知道编译过程有链接的时候,就会从main_main去找main.main。这个时候,才真正执行到我们程序员写的代码中。 go:linkname main_main main.main

1元购买《Go语言+支付宝》实战课

Go+支付宝实战课

Go语言微服务学习路线图

Go语言微服务学习路线图
添加微信 公众号更多内容
wechat gzh

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK