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 🙂