go语言设计与实现-常用关键字-阅读笔记
for和range
Go 语言中的经典循环在编译器看来是一个 OFOR
类型的节点,这个节点由以下四个部分组成:
- 初始化循环的
Ninit
; - 循环的中止条件
Left
; - 循环体结束时执行的
Right
; - 循环体
NBody
:
for Ninit; Left; Right { NBody }
- 分析遍历数组和切片清空元素的情况;
- 分析使用
for range a {}
遍历数组和切片,不关心索引和数据的情况;
ha := a hv1 := 0 hn := len(ha) v1 := hv1 for ; hv1 < hn; hv1++ { ... }
- 分析使用
for i := range a {}
遍历数组和切片,只关心索引的情况;
ha := a hv1 := 0 hn := len(ha) v1 := hv1 for ; hv1 < hn; hv1++ { v1 := hv1 ... }
- 分析使用
for i, elem := range a {}
遍历数组和切片,关心索引和数据的情况;
ha := a hv1 := 0 hn := len(ha) v1 := hv1 for ; hv1 < hn; hv1++ { tmp := ha[hv1] v1, v2 := hv1, tmp ... }
对于所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新的变量 ha
,在赋值的过程中就发生了拷贝,所以我们遍历的切片已经不是原始的切片变量了。
而遇到这种同时遍历索引和元素的 range 循环时,Go 语言会额外创建一个新的 v2
变量存储切片中的元素,循环中使用的这个变量 v2 会在每一次迭代被重新赋值,在赋值时也发生了拷贝。
因为在循环中获取返回变量的地址都完全相同,所以会发生神奇的指针一节中的现象。所以如果我们想要访问数组中元素所在的地址,不应该直接获取 range 返回的变量地址 &v2
,而应该使用 &a[index]
这种形式。
在遍历哈希表时,编译器会使用 runtime.mapiterinit
和 runtime.mapiternext
两个运行时函数重写原始的 for/range 循环:
ha := a hit := hiter(n.Type) th := hit.Type mapiterinit(typename(t), ha, &hit) for ; hit.key != nil; mapiternext(&hit) { key := *hit.key val := *hit.val }
遍历字符串的过程与数组、切片和哈希表非常相似,只是在遍历时会获取字符串中索引对应的字节并将字节转换成 rune
。我们在遍历字符串时拿到的值都是 rune
类型的变量,for i, r := range s {}
的结构都会被转换成如下所示的形式:
ha := s for hv1 := 0; hv1 < len(ha); { hv1t := hv1 hv2 := rune(ha[hv1]) if hv2 < utf8.RuneSelf { hv1++ } else { hv2, hv1 = decoderune(h1, hv1) } v1, v2 = hv1t, hv2 }
一个形如 for v := range ch {}
的语句最终会被转换成如下的格式
ha := a hv1, hb := <-ha for ; hb != false; hv1, hb = <-ha { v1 := hv1 hv1 = nil ... }
select
select
是一种与 switch
相似的控制结构,与 switch
不同的是,select
中虽然也有多个 case
,但是这些 case
中的表达式必须都是 Channel 的收发操作。
select
能在 Channel 上进行非阻塞的收发操作;select
在遇到多个 Channel 同时响应时会随机挑选case
执行;(随机的引入就是为了避免饥饿问题的发生)
- 当存在可以收发的 Channel 时,直接处理该 Channel 对应的
case
; - 当不存在可以收发的 Channel 是,执行
default
中的语句;
defer
defer 关键字的调用时机以及多次调用 defer 时执行顺序是如何确定的;(后进先出)
defer 关键字使用传值的方式传递参数时会进行预计算,导致不符合预期的结果(调用 defer 关键字会立刻对函数中引用的外部参数进行拷贝)
想要解决这个问题的方法非常简单,我们只需要向 defer 关键字传入匿名函数
defer 关键字的运行时实现分成两个部分:
runtime.deferproc 函数负责创建新的延迟调用;
runtime.deferreturn 函数负责在函数调用结束时执行所有的延迟调用;
编译期;
将 defer 关键字被转换 runtime.deferproc;
在调用 defer 关键字的函数返回之前插入 runtime.deferreturn;
运行时:
runtime.deferproc 会将一个新的 runtime._defer 结构体追加到当前 Goroutine 的链表头;
runtime.deferreturn 会从 Goroutine 的链表中取出 runtime._defer 结构并依次执行;
后调用的 defer 函数会先执行:
后调用的 defer 函数会被追加到 Goroutine _defer 链表的最前面;
运行 runtime._defer 时是从前到后依次执行;
函数的参数会被预先计算;
调用 runtime.deferproc 函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算;
pannic和recover
panic 能够改变程序的控制流,函数调用panic 时会立刻停止执行函数的其他代码,并在执行结束后在当前 Goroutine 中递归执行调用方的延迟函数调用 defer;
recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥任何作用;
recover 只有在发生 panic 之后调用才会生效。我们需要在 defer 中使用 recover 关键字。
make和new
make 的作用是初始化内置的数据结构,也就是切片、哈希表和 Channel
new 的作用是根据传入的类型在堆上分配一片内存空间并返回指向这片内存空间的指针
在编译期间的类型检查阶段,Go 语言就将代表 make 关键字的 OMAKE 节点根据参数类型的不同转换成了 OMAKESLICE、OMAKEMAP 和 OMAKECHAN 三种不同类型的节点,这些节点会调用不同的运行时函数来初始化相应的数据结构