• for range 的迭代变量会被重用
  • range expression 的副本参与 iteration

1. iteration variable 重用

for range 的 idiomatic(惯用)使用方式是使用 short variable declaration(:=) 形式在 for expression 中声明 iteration variable,但需要注意的是这些variable在每次循环体中都会被重用,而不是重新声明。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func main() {
	var arr = [...]int{1, 2, 3, 4, 5} // 声明包含有5个int类型元素的数组

	for i, v := range arr {
		go func() {
			time.Sleep(3 * time.Second)
			fmt.Printf("index: %v, value: %v\n", i, v)
		}()
	}

	time.Sleep(10 * time.Second)
}

// output:
// i=4, v=5, 0xc042010058 0xc042010060
// i=4, v=5, 0xc042010058 0xc042010060
// i=4, v=5, 0xc042010058 0xc042010060
// i=4, v=5, 0xc042010058 0xc042010060
// i=4, v=5, 0xc042010058 0xc042010060
//
// result:
// * 从输出结果可以看到: for 循环中,开启的 go 程中 i,v 的输出值是 for...range 遍历
// 完成后的最终值,而不是 go 程启动时所传入的值
// * 其次,for...range 遍历过程中,迭代变量 i,v 始终指向同一块内存空间(i:0xc042010058,
// v:0xc042010060), 说明i,v并没有重新初始化而是在重用

Note: 在上面的代码中,for循环启动的各个goroutine输出的 i, v 值都是 for range 循环结束后 i, v 的最终值,而不是各个goroutine启动时的 i, v 值。 一个可行的 fix 的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
	for i, v := range arr { // 将i,v保存在匿名函数(闭包)所声明的作用域内
		go func(i, v int) {
			time.Sleep(3 * time.Second)
			fmt.Printf("i=%v, v=%v\n", i, v)
		}(i, v)
	}
	
	// result:
	// 循环中,go 程启动时,传入的参数(i,v)会被记录在内存中,所以在go程执行时
	// 输出的 i,v 每次都是不同的值即传入的值且指向了不同的内存地址

2. range expression 副本参与 iteration

range 后面接收的表达式的类型包括:array、point to array、slice、map、string、channel(有读权限的)

2.1. array

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func arrayRangeExpression(arr [5]int) {
	var r [5]int
	fmt.Println("a =", arr)

	for i, v := range arr { // range 'arr' is copy from arr
		if i == 0 {
			arr[1] = 12
			arr[2] = 13
		}
		r[i] = v
	}
	fmt.Println("r =", r)
	fmt.Println("a =", arr)

	// except output:
	// a = [1 2 3 4 5]
	// r = [1 12 13 4 5] 然而,这里 r 输出并不是这样的
	// a = [1 12 13 4 5]
	//
	// actual output:
	// a = [1 2 3 4 5]
	// r = [1 2 3 4 5]
	// a = [1 12 13 4 5]
	//
	// result:
	// v 是range表达式 arr 的副本 `arr`中取出来的值,因此,在 if i==0 {} 作用域内
	// 对 arr[1] arr[2]元素值做修改不会影响 r 的结果
}

Note: 我们原以为在第一次 iteration,也就是 i=0 时,我们对a的修改(arr[1]=12, arr[2]=13)会在第二次,第三次循环中被 v 取出,但结果却是v取出的依旧是a被修改前的值:2和3。这就是 for…range 的一个不大不小的坑: range expression 副本参与循环。也就是说在上面这个例子里。正真参与循环的是 arr 的副本,而不是正真的 arr。

Go 中的数组在内部表示为连续的字节序列,虽然长度是Go数组类型的一部分,但长度并不包含在数据的内部表示的部分中,而是由编译器在编译期计算出来。 这个例子中,对range表达式的拷贝,即对一个数据的拷贝,arr’ 则是Go临时分配的连续的字节序列,与 arr 完全不是一块内存。因此,无论 arr 被如何修改,其副本arr’ 依旧保持原值,并且参与循环的是arr’。所以,v从arr’中取出的仍旧是arr的原值,而非修改后的值。

Note: 但是在 for i:=0; i < len(arr); i++ {} 这类循环结构中,直接操作的是原数据的值,并不是一份拷贝,是可以修改数据值的。

2.2. pointer to array

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// pointerToArrayRangeExpression
func pointerToArrayRangeExpression(arr [5]int) {
	var r [5]int

	fmt.Println("pointerToArrayRangeExpression result:")
	fmt.Println("a =", arr)

	for i, v := range &arr {
		if i == 0 {
			arr[1] = 12
			arr[2] = 13
		}
		r[i] = v
	}
	fmt.Println("r =", r)
	fmt.Println("a =", arr)
	println()
	
	// output:
	// pointerToArrayRangeExpression result:
	// a = [1 2 3 4 5]
	// r = [1 12 13 4 5]
	// a = [1 12 13 4 5]
}

Note: 我们看到这次 r 数组的值与最终a被修改后的值一致了。这个例子中使用了 *[5]int 作为 range 的表达式,其副本依旧是一个指向原始数组 arr 的指针, 因此后续所有循环中均是 &arr 指向的的原始数组的指针参与计算,因此 v 能从 &arr 指向的原始数组中取出 arr 修改后的值。

idiomatic go 建议我们尽可能的使用slice替换掉array的使用

2.3. slice

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// sliceRangeExpression
func sliceRangeExpression(arr [5]int) {
	var r [5]int

	fmt.Println("sliceRangeExpression result:")
	fmt.Println("a =", arr)

	for i, v := range &arr {
		if i == 0 {
			arr[1] = 12
			arr[2] = 13
		}
		r[i] = v
	}
	fmt.Println("r =", r)
	fmt.Println("a =", arr)
	println()

	// output:
	// sliceRangeExpression result:
	// a = [1 2 3 4 5]
	// r = [1 12 13 4 5]
	// a = [1 12 13 4 5]
}

Note: 这里slice实现了预期的要求。 那 slice是如何做到的呢?

  • slice 在 go 的内部表示为一个 struct, 由 (*T, len, cap) 组成,其中 *T 指向slice对应的 underlying(底层) array的指针,len是slice当前的长度,cap是slice的最大容量。当range进行expression复制时,它实际上复制的是一个 slice, 也就是那个 struct。副本 struct 中的 *T 依旧指向原 slice 对应的 array,为此对slice的修改都反映到 underlying array arr上去了,v 从副本struct中 *T 指向的 underlying array 中获取数组元素,也就得到了修改后的元素值。

slice 与 array还有一个不同点:就是 slice 的len在运行时可以被改变,而array得len是一个常量,不可改变。那么len变化的 slice 对 for range 有何影响呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// sliceLenChangeRangeExpression 测试在for...range迭代过程中,切片的长度发生
// 变化将会产生什么结果
func sliceLenChangeRangeExpression(a []int) {
	var r = make([]int, 0)

	fmt.Println("sliceLenChangeRangeExpression result:")
	fmt.Println("a =", a)

	for i, v := range a {
		if i == 0 {
			a = append(a, 6, 7)
		}
		r = append(r, v)
	}
	fmt.Println("r =", r)
	fmt.Println("a =", a)
	println()

	// output:
	// liceLenChangeRangeExpression result:
	// a = [1 2 3 4 5]
	// r = [1 2 3 4 5]
	// a = [1 2 3 4 5 6 7]
}

Note: 在这个例子中,原slice a在 for range过程中被附加了两个元素6和7, 其中,len有5增加到了7,但是对于r却没有产生影响。是因为在 a 的副本 a’ 的内部表示的 struct 中,len 字段并没有改变,依旧是 5。因此 for…range只会循环5次,也就只获取a对应的underlying数组的前5个元素。

range 副本行为会带来一些性能上的消耗,尤其是当range expression的类型的为数组时,range需要复制整个数组;而当 range expression 类型为 pointer to array 或 slice时,这个消耗将小得多,仅仅需要复制一个指针或一个slice的内部表示(一个struct即可)。

2.4. 其他 range expression 类型

对于 range 后面的其他表达式类型,比如 string,map,channel,for range依旧会创建副本参与计算

string

对 string 来说,由于 string 的内部表示为 struct {*byte, len}, 并且 string 本身是 immutable(一成不变的),因此其行为和消耗和 slice expression类似。不过 for…range 对于 string 来说,每次循环的单位是 rune(code point的值),而不是byte,index为迭代字符码点的第一个字节的 position:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var s := "中国人"

for i, v := range s {
	fmt.Printf("%d %s 0x%x\n", i, string(v), v)
}

// output:
// 0 中 0x4e2d
// 3 国 0x56fd
// 6  0x4eba

map

对于 map 来说,map 内部表示为一个指针,指针副本也指向真实的map, 因此for range操作均操作的是源map

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// mapRangeExpression 测试 map 作为 for...range 的迭代变量,参与计算时将会
// 发生什么变化
func mapRangeExpression() {
	var m = map[string]int{
		"tony": 21,
		"tom":  22,
		"jim":  23,
	}
	var cnt int

	//for k, v := range m { // m' is copy from m(map[string]int)
	//	if cnt == 0 {
	//		delete(m, "tony")
	//	}
	//	cnt++
	//	fmt.Println(k, v)
	//}
	//fmt.Println("cnt:", cnt)
	//fmt.Println("---------")

	// output:
	// tony 21
	// tom 22
	// jim 23
	// cnt: 3
	//   or
	// tom 22
	// jim 23
	// cnt: 2

	cnt = 0
	for k, v := range m { // m' is copy from m(map[string]int)
		if cnt == 0 {
			m["lucy"] = 24
		}
		cnt++
		fmt.Println(k, v)
	}
	fmt.Println("cnt:", cnt)

	// output:
	// tony 21
	// tom 22
	// jim 23
	// lucy 24
	// cnt: 4
	//  or
	// tony 21
	// tom 22
	// jim 23
	// cnt: 3
}

Note:

  • 如果map中的某项在循环到达前被在循环体中删除了,那么它可能不会被iteration variable获取到
  • 如果在循环体中新建一个map元素项,那该项元素可能出现在后续循环中,也可能不出现

channel

对于 channel 来说,channel 内部表示为一个指针,channel的指针副本其实指向真是的channel

for…range最终以阻塞读的方式阻塞在channel expression上(即便是buffered channle,当channel中无数据时,for…range也会阻塞在channel),直到channel关闭

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func channelRangeExpression() {
	var c = make(chan int)

	go func() {
		time.Sleep(time.Second * 3)
		c <- 1
		c <- 2
		c <- 3
		close(c)
	}()

	flag := true
	start := time.Now()
	for v := range c { // 阻塞3秒,后读取数据
		if flag {
			flag = false
			end := time.Since(start)
			fmt.Println(end)
		}
		fmt.Println(v)
	}

	//output:
	//3.0004311s
	//1
	//2
	//3
}

Note: channel变量为 nil, 则for…range将永远阻塞

See Also

Thanks to the authors 🙂