undefined

for 和 range

一、遍历切片或数组

1. 遍历数组的同时修改数组元素

1
2
3
4
5
6
7
8
func main() {
arr := []int{1,2,3}
for _, v := range arr {
arr = append(arr, v)
}
fmt.Println(arr)
}
输出:[1 2 3 1 2 3]

如上,结果表明,循环只遍历了原始切片中的三个元素,在遍历切片时追加的元素不会增加循环的执行次数,因此循环最终还是会停下来。

原理:Go语言源码中,对于所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新变量,在赋值过程中就发生了复制,并且又使用 len 关键字预先获取了切片的长度,所以在循环中追加新元素不会改变循环执行的次数。

2. range 返回变量的地址

1
2
3
4
5
6
7
8
9
10
11
func main() {
arr := []int{1,2,3}
newArr := []*int{}
for _, v := range arr {
newArr = append(newArr, &v)
}
for _, v := range newArr {
fmt.Printf("%d ", *v)
}
}
输出:3 3 3

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

因此我们想访问数组中元素的地址时,应该使用 &arr[index] 这种形式

二、遍历哈希表

遍历哈希表时,编译器会根据 range 返回值的数量在循环体中插入需要的赋值语句。

  • for k := range hash{} ,会插入 k := *hit.key
  • for k, v := range hash{},会插入 k := *hit.key, v := *hit.value

遍历前,会通过 runtime.fastrand 生成一个随机数随机选择一个遍历桶的起始位置。Go 语言不希望使用者依赖固定的遍历顺序,因此引入了随机数保证遍历的随机性。那么遍历过程中,通过随机数先选出一个正常桶开始遍历,随后遍历所有溢出桶,因为哈希表中正常桶和溢出桶在内存中是连续的,最后按照索引顺序遍历哈希表中其他的桶,直到遍历完所有桶。

三、遍历字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
str := "hello 世界"
for i := 0; i < len(str); i++ {
fmt.Printf("%c", str[i])
}
fmt.Println()
for _, v := range str {
fmt.Printf("%c", v)
}
}
输出:
hello ä¸ç
hello 世界

如上,遍历字符串分为两种情况

  • 使用下标访问字符串,那将会得到字符串中每个字节存储的元素,无法正常输出汉字
  • 使用 range 访问字符串,就会转换成 rune 类型,rune 类型的定义:type rune = int32,rune 类型就是为了区分字符值和整数值。因此如果当前 rune 是 ASCII 的,那么只会占用 1 字节长度,每次循环体运行之后只需要将索引加一;但是如果当前 rune 占用多个字节,就会使用 runtime.decoderune 函数解码。循环运行后将索引加 N 个字节。

四、遍历 channel

使用 for range 也可以遍历 channel。如 for v := range ch {} 语句,会循环从 channel 中取出待处理的值,并且会调用 runtime.chanrecv2 阻塞当前协程。