range/

directory
v0.0.0-...-ce13bd1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2023 License: MIT

README

for range

Go 中的经典循环在编译器由以下四个部分组成:

  • 初始化循环的 Ninit;
  • 循环的继续条件 Left;
  • 循环体结束时执行的 Right;
  • 循环体 NBody
for Ninit; Left; Right {
    NBody
}

范围循环在 Go 语言中更常见、实现也更复杂。这种循环同时使用 forrange 两个关键字,编译器会在编译期间将所有 for-range 循环变成经典循环。

所有的 for-range 循环都会被 cmd/compile/internal/gc.walkrange 转换成不包含复杂结构、只包含基本表达式的语句。

数组和切片

对于数组和切片来说,Go 语言有三种不同的遍历方式:

  • 分析遍历数组和切片清空元素的情况;
  • 分析使用 for range a {} 遍历数组和切片,不关心索引和数据的情况;
  • 分析使用 for i := range a {} 遍历数组和切片,只关心索引的情况;
  • 分析使用 for i, elem := range a {} 遍历数组和切片,关心索引和数据的情况;
	switch t.Etype {
	default:
		Fatalf("walkrange")

	case TARRAY, TSLICE:
		// 优化遍历数组或者切片并删除全部元素的逻辑
		if arrayClear(n, v1, v2, a) {
			lineno = lno
			return n
		}
// 原代码
for i := range a {
	a[i] = zero
}

// 优化后
if len(a) != 0 {
	hp = &a[0]
	hn = len(a)*sizeof(elem(a))
	memclrNoHeapPointers(hp, hn)
	i = len(a) - 1
}

相比于依次清除数组或者切片中的数据,Go 语言会直接使用 runtime.memclrNoHeapPointers 或者 runtime.memclrHasPointers 清除目标数组 内存空间中的全部数据,并在执行完成后更新遍历数组的索引。

ORANGE 节点的处理:

		ha := a

		hv1 := temp(types.Types[TINT])
		hn := temp(types.Types[TINT])

		init = append(init, nod(OAS, hv1, nil))
		init = append(init, nod(OAS, hn, nod(OLEN, ha, nil)))

		n.Left = nod(OLT, hv1, hn)
		n.Right = nod(OAS, hv1, nod(OADD, hv1, nodintconst(1)))

		if v1 == nil {
			break
		}

如果循环是 for range a {},那么就满足了上述代码中的条件 v1 == nil,即循环不关心数组的索引和数据,这种循环会被编译器转换成如下形式:

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
    ...
}

如果在遍历数组时需要使用索引 for i := range a {},那么编译器会继续会执行下面的代码:

		if v2 == nil {
			body = []*Node{nod(OAS, v1, hv1)}
			break
		}

v2 == nil 意味着调用方不关心数组的元素,只关心遍历数组使用的索引。与第一种循环相比,这种循环在循环体中添加了 v1 := hv1 语句,传 递遍历数组时的索引:

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
    v1 = hv1
    ...
}

同时去遍历索引和元素也很常见。处理这种情况会使用下面这段的代码:

		tmp := nod(OINDEX, ha, hv1)
		tmp.SetBounded(true)
		a := nod(OAS2, nil, nil)
		a.List.Set2(v1, v2)
		a.Rlist.Set2(hv1, tmp)
		body = []*Node{a}
	}
	n.Ninit.Append(init...)
	n.Nbody.Prepend(body...)

	return n

使用者同时关心索引和切片的情况。它不仅会在循环体中插入更新索引的语句,还会插入赋值操作让循环体内部的代码能够访问数组中的元素:

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

对于所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新变量 ha,在赋值的过程中就发生了拷贝,而我们又通过 len 关键字预先 获取了切片的长度,所以在循环中追加新的元素也不会改变循环执行的次数

循环中使用的这个变量 v2 会在每一次迭代被重新赋值而覆盖,赋值时也会触发拷贝

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

因为在循环中获取返回变量的地址都完全相同,所以输出会是 3 3 3

Map

在遍历 map 时,编译器会使用 runtime.mapiterinitruntime.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
}

上述代码是展开 for key, val := range hash {} 后的结果。

三种不同的情况分别向循环体插入了不同的赋值语句。

  • for range hash {}
  • for k := range hash {}
  • for k, v := range hash {}

遍历哈希表时会使用 runtime.mapiterinit 函数初始化遍历开始的元素

func mapiterinit(t *maptype, h *hmap, it *hiter) {
	it.t = t
	it.h = h
	it.B = h.B
	it.buckets = h.buckets

	r := uintptr(fastrand())
	it.startBucket = r & bucketMask(h.B)
	it.offset = uint8(r >> h.B & (bucketCnt - 1))
	it.bucket = it.startBucket
	mapiternext(it)
}

初始化 runtime.hiter 结构体中的字段,并通过 runtime.fastrand 生成一个随机数帮助我们随机选择一个遍历桶的起始位置。 Go 团队在设计哈希表的遍历时就不想让使用者依赖固定的遍历顺序,所以引入了随机数保证遍历的随机性。

字符串

遍历字符串的过程与数组、切片和哈希表非常相似,只是在遍历时会获取字符串中索引对应的字节并将字节转换成 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(ha, hv1)
    }
    v1, v2 = hv1t, hv2
}

chan

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 变量中的数据,然后重新陷入阻塞等待新数据;

hb != false 也就意味着,终止条件是 chan 关闭,这就是为什么 range 可以持续从一个通道接收值,直到 chan 关闭。如果 chan 没有元素, 或者 chan 为 nil,则会阻塞 goroutine

Directories

Path Synopsis
issues
v1 command
v2 command
v3 command
v4 command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL