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

1.Java VS Go语言

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

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

我们把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语言是如何编译成二进制文件的

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 Tree
1. 把源代码变成文本,然后把每个单词拆分出来
2. 把每个单词变成语法树
3. 类型检查、类型推断、类型匹配、函数调用、逃逸分析

分析阶段

  1. 词法检查分析、语法检查分析、语义检查分析)
  2. 生成中间码生成(SSA代码,类似汇编)。
    执行export GOSSAFUNC=main,代表你要看main函数的ssa代码,然后执行go build,会生成ssa.html

      图1. ![ssa1](https://tva1.sinaimg.cn/large/008vxvgGly1h8n9qsqpduj31k70u0k3e.jpg)
      图2. ![ssa2](https://tva1.sinaimg.cn/large/008vxvgGly1h8n9r7vr1xj30ti0emtc0.jpg)
  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. 启用GC
  5. fn := main_main 这是隐式的调用,因为linker运行时不知道主包的地址。在之前的学习,我们知道编译过程有链接的时候,就会从main_main去找main.main。这个时候,才真正执行到我们程序员写的代码中。 go:linkname main_main main.main
0 条评论
请不要发布违法违规有害信息,如发现请及时举报或反馈
还没有人评论呢,速度抢占沙发!
相关文章
  • 这本书是写什么的? 这是一本 Go 语言快速入门手册,目标读者是有任一编程语言基础,希望以最快的时间 (比如一个周末) 入门 Go 语言。 这本书应该怎么读? 书中几乎没有较长篇幅的理论知识,更多的是...

  • Go 箴言 不要通过共享内存进行通信,通过通信共享内存 并发不是并行 管道用于协调;互斥量(锁)用于同步 接口越大,抽象就越弱 利用好零值 空接口 interface{} 没有任何类型约束 Gofm...

  • 服务端 package main import ( "errors" "fmt" "log" "net" "net/rpc" "net/rpc/jsonrpc" "os" ) // ...

  • 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是...

  • 什么是传值(值传递) func main() { i:=10 ip:=&i fmt.Printf("原始指针的内存地址是:%p\n",&ip) modify(ip) fmt.Println(...

  • 一 Consul介绍 Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。 Consul是分布式的、高可用的、可横向扩展的。 1. 注册中心Consul基本介绍 ...

  • 概述建议先阅读 goroutine 小节。Go 箴言: 不要通过共享内存来通信,而要通过通信来共享内存。goroutine 是 Go 程序并发执行的实体,channel (通道) 则是它们之间的连接,...

  • 自定义类型关键字 type, 主要用来对同一种类型进行抽象。语法规则type 自定义类型名称 具体类型 # 例子 type Number int同时定义多个自定义类型package ma...

  • 前言在前面多篇 Go 系列文章中,我们了解到,Go 语言脱胎于 C 语言,这就意味着在某些更底层的细节中,我们可以使用 C 语言实现,然后通过 Go 来调用相关的 C 代码。其实这一特点,在 Java...

  • 往期回顾: Go语言开发小技巧&易错点100例(一) 本期看点(技巧类用【技】表示,易错点用【易】表示): (1)Go Module中对依赖库版本的升级与降级【技】 (2...

  • 协程介绍 什么是协程? 协程,又称微线程,英文为 Coroutine。 协程可以理解为用户态线程,是比线程更小的执行单元。为啥说它是一个执行单元?因为它自带 CPU 上下文。这样只要在合适的时机,我们...

  • Go语言是一种高效、快速、简洁的编程语言,近年来越来越受到开发者的欢迎。由于Go语言的快速发展,出现了很多的优秀框架来支持Go应用程序的开发。以下是一些目前比较流行的Go语言框架: 1. Gin:Gi...

  • 一 什么是ELK ELK是三个[开源软件]的缩写,分别表示:Elasticsearch , Logstash, Kibana , 它们都是开源软件,新增了一个Beats。 Elasticsearc...

  • 这篇文章比较硬核,爆肝5千字,把之前整理的知识点都串起来了。建议先收藏,慢慢看。 前言 上一篇文章 #【Go WEB进阶实战】开源的电商前后台API系统 很受大家欢迎,有好多小伙伴私信我问题:“gt...

  • 概述函数 是将一个或者一类问题包装为一个代码块,可以被多次调用,提高代码重用性。Go 函数中声明、定义、参数、返回值这些基础概念,和其他编程语言中的一致,这里不再赘述。语法规则Go 函数支持单个返回值...

  • 一、前言很多人入门的go使用的第一个框架有可能就是go-micro,特别是go-micro/config库,已经成为了,很多其他框架写config配置的默认参考结构,今天我们就来学习一下,如何使用go...

  • dongle 是一个轻量级、语义化、对开发者友好的 Golang 编码解码和加密解密库Dongle 已被 awesome-go 收录, 如果您觉得不错,请给个 star 吧github.com/gol...

  • 输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。 假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 示例 1: Input: preorder = [3,9,20,...

  • 一 前言 在utils目录中新建一个swap.go文件 swap.go中主要存放一些可以使用到的一些公共函数 导包: import ( "account/domain/model...

  • 概述调用 os 包即可。例子创建文件package main import ( "fmt" "os" ) func main() { file, err := os.Cre...