Golang 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
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main

import "fmt"

func test1() {
slice1 := []int{1, 2, 3, 4}
for _, val := range slice1 {
val++
}

fmt.Println(slice1)
}

func test2() {
slice2 := []int{1, 2, 3, 4}
for k, _ := range slice2 {
slice2[k]++
}

fmt.Println(slice2)
}

func test3() {
slice3 := []int{1, 2, 3, 4}
for i := 0; i < len(slice3); i++ {
slice3[i]++
}

fmt.Println(slice3)
}

type Student struct {
Age int
}

func test4() {
arr := [3]Student{
{
Age: 10,
},
{
Age: 15,
},
{
Age: 20,
},
}
for _, v := range arr {
v.Age = 0
}
fmt.Println(arr)
}

func test5() {
arr := [3]Student{
{
Age: 10,
},
{
Age: 15,
},
{
Age: 20,
},
}
for i := range arr {
arr[i].Age = 0
}
fmt.Println(arr)
}

func main() {
test1() // [1 2 3 4]
test2() // [2 3 4 5]
test3() // [2 3 4 5]
test4() // [{10} {15} {20}]
test5() // [{0} {0} {0}]
}

其中,test1(), test4()中的修改并未对原数据产生影响,而 test2() , test3() 和test5() 中的修改真正改变了原数据。主要原因:

  • val是slice内元素的副本,对val的改变不会导致slice1内元素的改变
  • 而在test2(), test3() 和test5() 中是直接对切片进行索引修改,改变了底层的数组

for range 原理

1
2
3
4
5
6
7
// Arrange to do a loop appropriate for the type.  We will produce
// for INIT ; COND ; POST {
// ITER_INIT
// INDEX = INDEX_TEMP
// VALUE = VALUE_TEMP // If there is a value
// original statements
// }

其中针对 slice 的编译方式如下:

1
2
3
4
5
6
7
8
9
// The loop we generate:
// for_temp := range
// len_temp := len(for_temp)
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = for_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }

golang官方的源码我们可以看到,针对slice,for range 做了一下事情:

  • 对要遍历的 Slice 做一个拷贝
  • 获取长度大小
  • 使用常规for循环进行遍历,返回值的拷贝,并把值存放到全局变量 index 和 value中

也就是说,对于 for k, val := range(slice) 环过程中,val 在循环内始终都是同一个全局变量

结合上面的结论,我们接下来再看一道题:

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
package main

import "fmt"

func test() {
s := []int{0, 1, 2, 3}
m := make(map[int]*int)

for index, value := range s {
m[index] = &value
}

printMap(m)
}

func printMap(m map[int]*int) {
for key, value := range m {
fmt.Printf("map[%v]=%v\n", key, *value)
}
}

func main() {
test()
}


// output
map[2]=3
map[3]=3
map[0]=3
map[1]=3

map直接存的是地址,因为在整个for index, value := range s 循环中,value都是同一个全局变量,地址是一样的,每次遍历该地址上的值都会被新的值覆盖掉,所以在遍历结束后,该地址存的值是切片上的最后一个元素3.

想要达到目的,每次进入循环体,都声明一个新变量valueCopy,并把value赋值给它,最后把新变量valueCopy的地址存到 m 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func test() {
s := []int{0, 1, 2, 3}
m := make(map[int]*int)

for index, value := range s {
valueCopy := value
m[index] = &valueCopy
}

printMap(m)
}

// output
map[0]=0
map[1]=1
map[2]=2
map[3]=3

原因是因为每次循环都声明新变量,对应的地址也是不一样的。

再看一个闭包,其原理一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"time"
)

func main() {
str := []string{"I","am","bobo"}
for _, v := range str{
// 每个goroutine的v的地址相同,都是为外部v的地址
go func() {
// 这里的v是引用外部变量v的地址
fmt.Println(v)
}()
}

time.Sleep(3 * time.Second)
}

// output
bobo
bobo
bobo

上面闭包要想实现输出不同的值,可利用函数的值传递性质:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"time"
)

func main() {
str := []string{"I","am","bobo"}
for _, v := range str{
// 把外部的v值拷贝给函数内部的v
go func(v string) {
fmt.Println(v)
}(v)
}

time.Sleep(3 * time.Second)
}

// output
I
am
bobo

对于slice

由 for range 的原理我们可以知道 for i, v := range x,进入循环前会对x的长度进行快照,决定一共循环len(x)那么多次。后面x的变动不会改变循环次数。通过i,以及最新的x,把x[i]赋予给v。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
)

func main() {
x := []int{1, 3, 5, 7, 9, 11, 13, 15}
fmt.Println("start with ", x)

for i, v := range x {
fmt.Println("The current value is", v)
x = append(x[:i], x[i+1:]...)
fmt.Println("And after it is removed, we get", x)
}
}

上面代码,在遍历切片的时候,每遍历一次就把该元素从切片中删掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//output 
The current value is 1
And after it is removed, we get [3 5 7 9 11 13 15]
The current value is 5
And after it is removed, we get [3 7 9 11 13 15]
The current value is 9
And after it is removed, we get [3 7 11 13 15]
The current value is 13
And after it is removed, we get [3 7 11 15]
The current value is 15
panic: runtime error: slice bounds out of range [5:4]

goroutine 1 [running]:
main.main()
/data1/htdocs/go_project/src/github.com/cnyygj/go_practice/Interview/for_range.go:13 +0x398
exit status 2

从输出我们可以看出,for range 的循环次数取决于循环前会对遍历目标的长度进行快照,并不会随着遍历目标长度的修改而改变。所以最终会出现切片溢出的panic

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
type Guest struct {
id int
name string
surname string
friends []int
}

func (self Guest) removeFriend(id int) {
for i, other := range self.friends {
if other == id {
self.friends = append(self.friends[:i], self.friends[i+1:]...)
break
}
}
}

func main() {
test := Guest{0, "Y", "bb", []int{1,2, 3, 4, 5}}
fmt.Println(test)
test.removeFriend(4)
fmt.Println(test)
}

// output
{0 Y bb [1 2 3 4 5]}
{0 Y bb [1 2 3 5 5]}

Guest.removeFriend()方法想要修改self(Guest type)的friend字段(slice type)。

但是用了value receiver, 只是改变了 一个 Guest type的副本的friends字段。原本的Guest对象的friend字段还有未被更改的slice元素。

如果要修改原本的Guest对象,需要用指针的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
func (self *Guest) removeFriend(id int) {
for i, other := range self.friends {
if other == id {
self.friends = append(self.friends[:i], self.friends[i+1:]...)
break
}
}
}


// output
{0 Y bb [1 2 3 4 5]}
{0 Y bb [1 2 3 5]}