Golang range 底层原理思考

问题

在如下代码中

func main() {
	str := "asd"
	fmt.Println(reflect.TypeOf(str), reflect.ValueOf(str))
	for _, b := range str {
		fmt.Println(reflect.TypeOf(b), reflect.ValueOf(b))
	}
	fmt.Println(reflect.TypeOf(str[0]), reflect.ValueOf(str[0]))
}

执行结果是

string asd
int32 97
int32 115
int32 100
uint8 97

可以看到在golang 的 for range 循环中,数据类型与我们预期的是不一致的,byte(uin8) 类型变成了 rune(int32)。

range 底层原理

在 Golang 中,for range 语句会被转换成最简单的 for 语句,再进行语义分析。

数组和切片

遍历数组和切片清空元素
// 原代码
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
}

这里进行了特殊的优化,会计算数组的字节长度,根据起始地址和长度调用runtime.memclrNoHeapPointers清空这一段内存,并在执行完成后更新遍历数组的索引。

for range a {} 不关心索引和数据的情况
// 优化后
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
    ...
}

由于原代码不需要获取数组的索引和元素,只需要使用数组或者切片的数量执行对应次数的循环,所以会生成一个最简单的 for 循环。

for i := range a {} 只关心索引的情况
// 优化后
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
    v1 = hv1
    ...
}

在循环体中添加了 v1 := hv1 语句,传递遍历数组时的索引

for i, elem := range a {} 关心索引和数据的情况
// 优化后
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
    tmp := ha[hv1]
    v1, v2 = hv1, tmp
    ...
}

遇到这种同时遍历索引和元素的 range 循环时,Go 语言会额外创建一个新的 v2 变量存储切片中的元素,循环中使用的这个变量 v2 会在每一次迭代被重新赋值而覆盖

哈希表

// 优化前
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
    ...	
}
遍历 map 的随机性

在这里插入图片描述
首先会随机选出一个绿色的正常桶开始遍历,随后遍历所有黄色的溢出桶,最后依次按照索引顺序遍历哈希表中其他的桶,直到所有的桶都被遍历完成。

字符串

// 优化前
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
    ...
}

遍历字符串的过程与数组、切片和哈希表非常相似,只是在遍历时会获取字符串中索引对应的字节并将字节转换成 rune。
如果当前的 rune 是 ASCII 的,那么只会占用一个字节长度,每次循环体运行之后只需要将索引加一,但是如果当前 rune 占用了多个字节就会使用 runtime.decoderune 函数解码

通道

// 优化前
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 变量中的数据,然后重新陷入阻塞等待新数据。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值