for range
使用 for-range 的控制结构最终也会被 Go 语言编译器转换成普通的 for 循环
现象
- 循环永动机
如果我们在遍历数组的同时修改数组的元素,能否得到一个永远都不会停止的循环呢?
func main() {
arr := []int{1, 2, 3}
for _, v := range arr {
arr = append(arr, v)
}
fmt.Println(arr)
}
还是只会遍历原始的元素
对于所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新变量 ha,在赋值的过程中就发生了拷贝,而我们又通过 len 关键字预先获取了切片的长度,所以在循环中追加新的元素也不会改变循环执行的次数,这也就解释了循环永动机提到的现象
- 神奇的指针
func main() {
arr := []int{1, 2, 3}
newArr := []*int{}
for _, v := range arr {
newArr = append(newArr, &v)
}
for _, v := range newArr {
fmt.Println(*v)
}
}
// 333
正确的做法应该是使用 &arr[i] 替代 &v
在循环中获取返回变量的地址都完全相同,最后面赋值为3,所以会发生神奇的指针的现象
range变量map顺序是随机的
不关心索引和值:for range a {}
只关心索引:for i := range a {}
关心索引和值 for i, v := range a {}
遍历字符串得到的值都是rune类型
select
select 能在 Channel 上进行非阻塞的收发操作
select 在遇到多个 Channel 同时响应时,会随机执行一种情况
非阻塞收发
在通常情况下,select 语句会阻塞当前 Goroutine 并等待多个 Channel 中的一个达到可以收发的状态。但是如果 select 控制结构中包含 default 语句,那么这个 select 语句在执行时会遇到以下两种情况:
1、当存在可以收发的 Channel 时,直接处理该 Channel 对应的 case
2、当不存在可以收发的 Channel 时,执行 default 中的语句
随机执行
同时有多个 case 就绪时 select 会随机选择一个执行
实现原理
- 直接阻塞
select 不存在任何的 case ,转换为调用runtime.block
- 单一管道
select 只存在一个 case ,编译器会将 select 改写成 if 条件语句
- 非阻塞操作
select 存在两个 case,其中一个 case 是 default
select 存在多个 case
常见流程
1、随机生成一个遍历的轮询顺序 pollOrder 并根据 Channel 地址生成锁定顺序 lockOrder
2、根据 pollOrder 遍历所有的 case 查看是否有可以立刻处理的 Channel
如果存在,直接获取 case 对应的索引并返回
如果不存在,创建 runtime.sudog 结构体,将当前 Goroutine 加入到所有相关 Channel 的收发队列,并调用 runtime.gopark 挂起当前 Goroutine 等待调度器的唤醒
3、当调度器唤醒当前 Goroutine 时,会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 runtime.sudog 对应的索引
defer
常被用于关闭文件描述符、关闭数据库连接以及解锁资源
defer 关键字的插入顺序是从后向前的,而 defer 关键字执行是从前向后的,这也是为什么后调用的 defer 会优先执行
函数的参数会被预先计算
创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算
1.1~1.12 都是在堆上分配
Go 语言团队在 1.13 中对 defer 关键字进行了优化,当该关键字在函数体中最多执行一次时,会将结构体分配到栈上并调用
Go 语言在 1.14 中通过开放编码(Open Coded)实现 defer 关键字,该设计使用代码内联优化 defer 关键的额外开销并引入函数数据 funcdata 管理 panic 的调用
得满足一下条件:
1、函数的 defer 数量少于或者等于 8 个
2、函数的 defer 关键字不能在循环中执行
3、函数的 return 语句与 defer 语句的乘积小于或者等于 15 个
编译期间在栈上初始化大小为 8 个比特的 deferBits 变量
延迟比特中的每一个比特位都表示该位对应的 defer 关键字是否需要被执行
panic recover
panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer
recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用
Go 语言中的 panic 是可以多次嵌套调用的
make new
make 的作用是初始化内置的数据结构,也就是我们在前面提到的切片、哈希表和 Channel
new 的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针
如果通过 var 或者 new 创建的变量不需要在当前作用域外生存,例如不用作为返回值返回给调用方,那么就不需要初始化在堆上