1、检查常量、类型和函数的类型;
2、处理变量的赋值;
3、对函数的主体进行类型检查;
4、决定如何捕获变量;
5、检查内联函数的类型;
6、进行逃逸分析;
7、将闭包的主体转换成引用的捕获变量;
8、编译顶层函数;
9、检查外部依赖的声明;
数组大小、对数组中的元素的读写在编译期间就已经进行了简化,由于数组的内存固定且连续,很多操作都会变成对内存的直接读写
切片的很多功能都是在运行时实现的了,无论是初始化切片,还是对切片进行追加或扩容都需要运行时的支持,
注:需要注意的是在遇到大切片扩容或者复制时可能会发生大规模的内存拷贝,一定要在使用时减少这种情况的发生 避免对程序的性能造成影响。
字符串在做拼接和类型转换等操作时时一定要注意性能的损耗,遇到需要极致性能的场景一定要尽量减少类型转换的次数
字符串和 []byte 中的内容虽然一样,但是字符串的内容是只读的,我们不能通过下标或者其他形式改变其中的数据,而 []byte 中的内容是可以读写的,无论从哪种类型转换到另一种都需要对其中的内容进行拷贝,而内存拷贝的性能损耗会随着字符串和 []byte 长度的增长而增长。
go tool compile -S -N -l main.go
1、通过堆栈传递参数,入栈的顺序是从右到左;
2、函数返回值通过堆栈传递并由调用者预先分配内存空间;
3、调用函数时都是传值,接收方会对入参进行复制再计算;
注:在传递数组或者内存占用非常大的结构体时,我们在一些函数中应该尽量使用指针作为参数类型来避免发生大量数据的拷贝而影响性能。
_type 是 Go 语言类型的运行时表示。下面是运行时包中的结构体,结构体包含了很多元信息,例如:类型的大小、哈希、对齐以及种类等。
type _type struct {
size uintptr // 存储了类型占用的内存空间,为内存空间的分配提供信息;
ptrdata uintptr // 能够帮助我们快速确定类型是否相等;
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
equal func(unsafe.Pointer, unsafe.Pointer) bool //用于判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从 typeAlg 结构体中迁移过来的
gcdata *byte
str nameOff
ptrToThis typeOff
}
eface 结构体表示第二种空接口
在类型断言中介绍 hash 字段的使用,在动态派发一节中介绍 fun 数组中存储的函数指针是如何被使用的
type itab struct {
// 32 bytes
inter *interfacetype
_type *_type
hash uint32 // 对 _type.hash 的拷贝,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 _type 是否一致
_ [4]byte
fun [1]uintptr // 用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的;
}
两种接口虽然都使用 interface 声明,但是由于后者在 Go 语言中非常常见,所以在实现时使用了特殊的类型。
需要注意的是,与 C 语言中的 void * 不同,interface{} 类型不是任意类型,如果我们将类型转换成了 interface{} 类型,这边变量在运行期间的类型也发生了变化,获取变量类型时就会得到 interface{}
*** | 结构体实现接口 | 结构体指针实现接口 |
---|---|---|
结构体初始化变量 | 通过 | 不通过 |
结构体指针初始化变量 | 通过 | 通过 |
func main() {
var c Duck = &Cat{
Name: "grooming"}
c.Quack()
c.(*Cat).Quack()
}
两次方法调用对应的汇编指令差异就是动态派发带来的额外开销,这些额外开销在有低延时、高吞吐量需求的服务中是不能被忽视的
我们来详细分析一下产生的额外汇编指令对性能造成的影响
*** | 直接调用 | 动态派发 |
---|---|---|
指针 | ~3.03ns | ~3.58ns |
结构体 | ~3.09ns | ~6.98ns |
所以,使用结构体来实现接口带来的开销会大于使用指针实现,而动态派发在结构体上的表现非常差,这也提醒我们应当尽量避免使用结构体类型实现接口;
使用反射来 动态修改变量、判断类型是否实现了某些接口以及动态调用方法;
运行时的反射能力,能够让程序操作不同类型的对象
Type 是反射包定义的一个接口,我们可以使用 reflect.TypeOf 函数获取任意变量的的类型
Value 被声明成了结构体,提供了获取或者写入数据的方法
所有方法基本都是围绕着 Type 和 Value 这两个类型设计的。
我们通过 reflect.TypeOf、reflect.ValueOf 可以将一个普通的变量转换成『反射』包中提供的 Type 和 Value,随后就可以使用反射包中的方法对它们进行复杂的操作。
reflect.TypeOf 函数将传入的变量隐式转换成 emptyInterface 类型并获取其中存储的类型信息 rtype
reflect.ValueOf 在该函数中我们先调用了 reflect.escapes 函数保证当前值逃逸到堆上,然后通过 reflect.unpackEface 方法从接口中获取 Value 结构体;reflect.unpackEface 函数会将传入的接口转换成 emptyInterface 结构体,然后将具体类型和指针包装成 Value 结构体并返回。
三大法则
1、从 interface{
} 变量可以反射出反射对象;
reflect.TypeOf 和 reflect.ValueOf 函数能转换成 interface{
} 类型
2、从反射对象可以获取 interface{
} 变量;
既然能够将接口类型的变量转换成反射对象,那么一定需要其他方法将反射对象还原成接口类型的变量
3、要修改反射对象,其值必须可设置;
由于 Go 语言的函数调用都是值传递的,所以我们只能先获取指针对应的 reflect.Value,
再通过 reflect.Value.Elem 方法迂回的方式得到可以被设置的变量
func main() {
i := 1
v := reflect.ValueOf(&i) // 函数获取变量指针
v.Elem().SetInt(10) // reflect.Value.Elem方法获取指针指向的变量 reflect.Value.SetInt方法更新变量的值
fmt.Println(i)
}
$ go run reflect.go
10
Go 语言遍历数组和切片时会复用变量:
数组:因为在循环中获取返回变量的地址都完全相同,所以会发生神奇的指针一节中的现象。
所以如果我们想要访问数组中元素所在的地址,不应该直接获取 range 返回的变量地址 &v2,
而应该使用 &a[index] 这种形式。
func main() {
arr := []int{
1, 2, 3}
newArr := []*int{
}
for i, _ := range arr {
newArr = append(newArr, &arr[i])
}
for _, v := range newArr {
fmt.Println(*v)
}
}
C 语言中的 select 关键字可以同时监听多个文件描述符的可读或者可写的状态
Go 语言中的 select 关键字也能够让 Goroutine 同时等待多个 Channel 的可读或者可写
在多个文件或者 Channel 发生状态改变之前,select 会一直阻塞当前线程或者 Goroutine
const (
caseNil = iota
caseRecv
caseSend
caseDefault
)
select 不存在任何的 case; // 空的 select 语句会直接阻塞当前的 Goroutine
select 只存在一个 case; // 将 select 改写成 if 条件语句
select 存在两个 case,其中一个 case 是 default; // 编译器就会认为这是一次非阻塞的收发操作
select 存在多个 case;// 该函数会将 case 中的所有 Channel 都转换成指向 Channel 的地址。我们会分别介绍非阻塞发送和非阻塞接收时,编译器进行的不同优化
OSEND 时,编译器会使用 if/else 语句和 runtime.selectnbsend 函数改写代码
最重要的就是用于选择待执行 case 的运行时函数 **runtime.selectgo**
1、随机生成一个遍历的轮询顺序 pollOrder 并根据 Channel 地址生成锁定顺序 lockOrder;
2、根据 pollOrder 遍历所有的 case 查看是否有可以立刻处理的 Channel;
- 如果存在就直接获取 case 对应的索引并返回;
- 如果不存在就会创建 runtime.sudog 结构体,将当前 Goroutine 加入到所有相关 Channel 的收发队列
3、并调用 runtime.gopark 挂起当前 Goroutine 等待调度器的唤醒;
4、当调度器唤醒当前 Goroutine 时就会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 runtime.sudog 结构对应的索引;
用于关闭文件描述符、关闭数据库连接以及解锁资源
常用中碰到的现象:
调用 defer 关键字会立刻对函数中引用的外部参数进行拷贝
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt))
time.Sleep(time.Second)
}
$ go run main.go
0s
func main() {
startedAt := time.Now()
defer func() {
fmt.Println(time.Since(startedAt)) }()
time.Sleep(time.Second)
}
$ go run main.go
1s
上面两种常见现象的背后原理:
编译期;
运行时:
后调用的 defer 函数会先执行:
函数的参数会被预先计算;
panic 能够改变程序的控制流,函数调用panic 时会立刻停止执行函数的其他代码,并在执行结束后在当前 Goroutine 中递归执行调用方的延迟函数调用 defer;
调用 panic 都会创建一个如下所示的数据结构存储相关信息
type _panic struct {
argp unsafe.Pointer //指向 defer 调用时参数的指针
arg interface{
} //调用 panic 时传入的参数
link *_panic //指向了更早调用的 runtime._panic 结构
// panic 函数可以被连续多次调用,它们之间通过 link 的关联形成一个链表
recovered bool //当前 runtime._panic 是否被 recover 恢复
aborted bool //当前的 panic 是否被强行终止
pc uintptr
sp unsafe.Pointer
goexit bool
}
编译器会将关键字 panic 转换成 runtime.gopanic:
1、创建新的 runtime._panic 结构并添加到所在 Goroutine _panic 链表的最前面;
2、在循环中不断从当前 Goroutine 的 _defer 中链表获取 runtime._defer 并调用 runtime.reflectcall 运行延迟调用函数;
3、调用 runtime.fatalpanic 中止整个程序;
崩溃和恢复流程:详细的需要结合编译源码和运行时查看,大神看待问题的角度
设计原理:多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的工作
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go handle(ctx, 500*time.Millisecond)
select {
case <-ctx.Done():
fmt.Println("main", ctx.Err())
}
}
func handle(ctx context.Context, duration time.Duration) {
select {
case <-ctx.Done():
fmt.Println("handle", ctx.Err())
case <-time.After(duration):
fmt.Println("process request with", duration)
}
}
运行时会根据状态的不同而做出不同的反应,所以我们在分析计时器时会从状态的维度去分析其实现原理
运行时通过系统监控来触发线程的抢占、网络的轮询和垃圾回收,保证 Go 语言运行时的可用性。系统监控能够很好地解决尾延迟的问题,减少调度器调度 Goroutine 的饥饿问题并保证计时器在尽可能准确的时间触发。
堆中的对象由内存分配器分配并由垃圾收集器回收