持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情
1. go底层运行过程
不会有人还以为程序运行的入门是main 函数吧。其实很多语言在main函数之前就已经做过很多事情了,下面咋们就分析分析go中的入门到执行的整个过程吧~~~
1.1 真正的入口
实际上是runtime/rt0_xxx.s 这个文件中, 有关runtime的叙述在上一节已经加以说明
比如在windows 平台上就是在runtime包中rt0_windows_amd64.s 这个汇编文件中,至于为什么编写成汇编语言,那肯定的汇编语言效率高呀~
这个的命令就是对应不同平台的,amd64 、386、arm实际上就是对应不同的芯片架构,amd64机器和inter64位机器都用一个文件,这里都是64位机器(业界习惯将x86-64架构说成amd64)
查看里面的接口,64架构的都是调用的同一个接口
1.2 如何查看汇编文件
我这里使用的IDE 是Goland,查看源码非常方便
比如我需要找rt0_amd64 有关的文件,因为是汇编文件,需要点击里面的Files, 这个窗口需要双击shitf 出来
之后跳转到这个汇编函数中,这里的意思就是将argc, argv 变量放入到寄存器中
然后就跳转到
rt0_go 这个函数中,这个比较核心了
- 先将argc,argv 参数放到栈上
- 初始化一个g0 的协程, 不是作为程序的第一个协程
- 类型检查啥的
- 在执行一个go中的方法, runtime.check(),运行时检测
- 将argc, argv 拷贝到go 中的代码上去
- runtime.osinit () 判断系统的字长,
- 调度器初始化 runtime.scheldinit()
- 取主函数的地址mainPC, 是runtime.main的地址, 然后new 一个协程
- runtime.main 中doinit --> 垃圾回收器--->main_main函数(就是用户的main包中的main方法)
1.3 运行流程梳理
- 跳入汇编函数 rt0_go 中
- 读取参数数量argc、argv 到栈上
- 初始化g0执行栈 (1. g0 是为了调度协程而产生的协程(母协程) 2. g0是每个go程序的第一个协程)
- 运行时检测check函数, runtime.check() 里面包含: 1) 检查各种类型的长度 2) 检查指针操作 3) 检查结构体字段的偏移量 4) 检查atomic原子操作 5) 检查CAS操作 6.)检查栈大小是否是2的幂次
- 参数初始化runtime.args 1)对命令行中的参数进行处理 2)参数数量赋值给argc int32 3)参数值复制给argv **byte
- 调度器初始化 runtime.schedinit 1)全局栈空间内存分配 2)加载命令行参数到os.Args 3)堆内存空间的初始化 4)加载操作系统环境变量 5)初始化当前系统线程 6)垃圾回收的参数初始化 7)算法初始化(map、hash) 8)设置process 数量
- 创建主协程(现在就有两个协程了)
1)创建一个新的协程,执行
runtime.main(这个是runtime中的main,和用户写的main函数不是同一个) 2) 放入 调度器等待调度 - 初始化M 初始化一个M,用来调度主协程的(后面在深入)
- 主协程执行主函数 1)执行runtime 包中的init方法 2) 启动GC 垃圾收集器 3)执行用户包依赖的init方法 4)执行用户主函数main.main() , 在这里就到了我们写的main 函数了
2. go 面向对象?
2.1 Is Go an object-oriented language?
可以说是, 也可以说不是
- go 允许OO(面向对象)的编程风格
- go 的struct 可以看作其它语言的class
- go 缺乏其他语言的继承结构的(那个叫组合)
- go 的接口与其他语言有很大差异
2.2 go 中 "class"
- go 中用struct 表示一类数据
- struct 每个实例并不是对象,而是此类型的值
- struct 也可以定义方法
2.3 go 中 "继承"
- go 并没有继承关系, 下面的例子其实是组合
type People struct {
name string
age int
}
type Man struct {
People
}
- 所谓go 的继承只是组合而己
type People struct {
name string
age int
}
type Man struct {
People
}
func (p People) walk() {
}
func main() {
m := Man{}
m.walk() // 这其实是一个语法糖, 真正底层是 通过下面的方式实现的
m.People.walk()
fmt.Println("nihao")
}
- 组合中的匿名字段,通过语法糖达成了类似继承的效果 像上面的只有类型没有名字的 就叫做匿名字段, Man 结构体中的People 例子
2.3 go 中 接口
- 接口可以定义一组行为相似的struct
- struct 并不是显式实现接口,而是隐式实现
在 People 结构体实现函数的一瞬间,编译器就能提示哪些类实现了接口,该类中实现了哪些接口
3. go中什么变量0字节
3.1 基本类型查看占用字节数
int: 大小跟随着系统字长(因此才有int32, int64这类型出来)
package main
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println(unsafe.Sizeof(int(1)))
}
// int 64位系统占8字节,32位系统占4字节 根据系统字长决定,
// int64 永远占8字节
指针: 指针的大小也是随着系统字长决定 在64位系统中,是地址总线和数据总线是64位
package main
import (
"fmt"
"unsafe"
)
func main() {
i := int(0)
p := &i
fmt.Println(unsafe.Sizeof(p))
}
其他基本类型也可以按照上面查询占用的字节数
3.2 占用0字节的类型----空结构体
- 单个空结构体是有地址,没有大小的 (地址被称为 zerobase)
package main
import (
"fmt"
"unsafe"
)
type K struct {
}
func main() {
a := K{}
fmt.Println(unsafe.Sizeof(a))
fmt.Printf("%p\n", &a) // 有地址没有长度
// 0
// 0xebc578 : 这是一个空结构体指向的地址,后面你会发现这个是固定的
}
- 中间含有其他类型,但空结构体独立出现时(不被包含在其他结构体中) 空结构体的地址均相同,均指向一个地址zerobase
package main
import (
"fmt"
"unsafe"
)
type K struct {
}
func main() {
a := K{}
b := int(0)
c := K{}
fmt.Println(unsafe.Sizeof(a))
fmt.Printf("%p\n", &a) // 有地址没有长度
fmt.Printf("%p\n", &b)
fmt.Printf("%p\n", &c)
//0
//0xbdc578
//0xc00000a0c0
//0xbdc578
}
我们看一下zerobase 的类型会发现是 uintptr
- 空结构体不是单独的出现时, 会出现这样的情况,根据依赖的结构体地址走
package main
import (
"fmt"
)
type K struct {
}
type F struct {
num1 K
num2 int32
}
func main() {
f := F{}
a := K{}
//fmt.Println(unsafe.Sizeof())
fmt.Printf("%p\n", &f.num1)
fmt.Printf("%p\n", &f.num2)
fmt.Printf("%p\n", &a)
}
// 0xc00000a0c0
// 0xc00000a0c0
// 0x28c578
3.3 空结构体用途
- 结合map 实现hashmap
func main() {
m := map[string]int{}
n := map[string]struct{}{} // hashset // key : null
n["a"] = struct{}{} // 值不占任何空间
}
- 结合channel 当作纯信号
func main() {
a := make(chan struct{}) // 只想发送信号,不想携带任何信息,这样就不占任何内存
}
参考: 后台服务器:course.0voice.com/v1/course/i…