Go语言设计与实现 -- 关键字for和range

如果我们查看汇编代码的话,可以发现,经过优化for-range循环的汇编代码和普通for的结构相同。也就是说,使用for-range的控制结构最终也会被Go语言编译器换成普通的for循环。

现象提出

现象1:循环永动机

func main() {
	arr := []int{1, 2, 3}
	for _, v := range arr {
		arr = append(arr, v)
	}
	fmt.Println(arr) // [1 2 3 1 2 3]
}

现象2:神奇的指针

func main() {
	arr := []int{1, 2, 3}
	newArr := []*int{}
	for _, v := range arr {
		newArr = append(newArr, &v)
	}
	for _, v := range newArr {
		fmt.Println(*v)
	}
}
// 3
// 3
// 3

正确的做法是使用&arr[i]替代&v,原因我们后面会讲解。

现象3:遍历清空数组

当我们想在Go语言中清空一个切片或者哈希表的时候,一般会使用以下方法将切片中的元素置为0

func main() {
    arr := []int{1, 2, 3}
    for i, _ := range arr {
        arr[i] = 0
    }
}

这样清空是非常消耗性能的,所以在编译的时候,编译器会直接优化成使用runtime.memclrNoHeapPointers来清空切片中的数据。

现象4:随机遍历

当我们用for-range遍历哈希表的时候,得到的顺序是不相同的。

for-range循环遍历

从编译器的视角来看,就是将ORANGE 类型的节点转换成OFOR节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G9lqF3l0-1671804211571)(D:\A\图片\板书\for-range.excalidraw.png)]

然后我们来看一看for-range的遍历

数组和切片

数组和切片和遍历通常情况下会有4种可能性:

  • 遍历数组和切片清空元素的情况
  • for range a {},不关心索引和值
  • for i := range a {},只关心索引
  • for i, elem := range a{},关心索引和值

第一种情况

也就是我们的现象3的解决。

Go会使用runtime.memclrNoHeapPointers或者runtime.memclrHasPointers清除目标数组内存空间中的全部数据

第二种情况

如果我们不关心索引和值的话,那么代码大概会被编译器转换成这个样子:

for range a {}为例子

ha := a // 这里不是a了,是拷贝给了ha
hv1 := 0 // hv1代表循环变量
hn := len(ha) // 我的遍历长度已经给你计算完毕了
v1 := hv1 // v1代表索引的值
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, j := range a{}为例子

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1 // 表示索引
v2 := nil // 表示值
for ; hv1 < hn; hv1++ {
    tmp := ha[hv1]
    v1, v2 = hv1, tmp
    ...
}

所以解释现象1:

我们可以看到对于所有的range循环,Go会在编译期间将长度提前计算好,所以循环次数是已经固定的,不会无限制增加。

解释现象2:

我们可以看到表示值的变量v2会在每一次迭代被重新赋值而覆盖,覆盖到最后就都是一个结果了。因此要得到正确的结果,我们不应该获取range返回的变量地址&v2,而是直接获取&arr[i]

哈希表

该图片来自面向信仰编程

这个代码是for key, val := range hash {}展开的结果:

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
}

编译器会根据range返回值的数量在循环体中插入需要的赋值语句。

Go语言设计与实现 -- 关键字for和range_第1张图片

在遍历的时候,Go会通过runtime.fastrand生成一个随机数,这样我们第一个遍历桶的起始位置每次都是不一样的,这就解释了现象4。

Go语言设计与实现 -- 关键字for和range_第2张图片

简单总结一下哈希表遍历的顺序,首先会选出一个绿色的正常桶开始遍历,随后遍历所有黄色的溢出桶,最后依次按照索引顺序遍历哈希表中其他的桶,直到所有的桶都被遍历完成。

字符串

具体过程和遍历数组和切片差不多

Channel

使用 range 遍历 Channel 也是比较常见的做法,一个形如 for v := range ch {} 的语句最终会被转换成如下的格式:

ha := a
hv1, hb := <-ha
for ; hb != false; hv1, hb = <-ha {
    v1 := hv1
    hv1 = nil
    ...
}

这里的代码可能与编译器生成的稍微有一些出入,但是结构和效果是完全相同的。该循环会使用 <-ch 从管道中取出等待处理的值,这个操作会调用 runtime.chanrecv2 并阻塞当前的协程,当 runtime.chanrecv2 返回时会根据布尔值 hb 判断当前的值是否存在:

  • 如果不存在当前值,意味着当前的管道已经被关闭;
  • 如果存在当前值,会为 v1 赋值并清除 hv1 变量中的数据,然后重新陷入阻塞等待新数据;

你可能感兴趣的:(Go设计与实现,golang,数据结构,开发语言)