Golang 中 for-range 的坑

1. 指针数据坑

range 到底有什么坑呢,我们先来运行一个例子吧。

package main

import (
	"fmt"
)

type user struct {
	name string
	age uint64
}

func main()  {
	u := []user{
		{"asong",23},
		{"song",19},
		{"asong2020",18},
	}
	n := make([]*user,0,len(u))
	for _,v := range u{
		n = append(n, &v)
	}
	fmt.Println(n)
	for _,v := range n{
		fmt.Println(v)
	}
}

这个例子的目的是,通过 **u **这个 slice 构造成新的 slice。我们预期应该是显示 u slice 的内容,但是运行结果如下:

[0xc0000a6040 0xc0000a6040 0xc0000a6040]
&{asong2020 18}
&{asong2020 18}
&{asong2020 18}

这里我们看到 n 这个 slice 打印出来的三个同样的数据,并且他们的内存地址相同。这是什么原因呢?先别着急,再来看这一段代码,我给他改正确他,对比之后我们再来分析,你们才会恍然大悟。

package main

import (
	"fmt"
)

type user struct {
	name string
	age uint64
}

func main()  {
	u := []user{
		{"asong",23},
		{"song",19},
		{"asong2020",18},
	}
	n := make([]*user,0,len(u))
	for _,v := range u{
		o := v
		n = append(n, &o)
	}
	fmt.Println(n)
	for _,v := range n{
		fmt.Println(v)
	}
}

细心的你们看到,我改动了哪一部分代码了嘛?对,没错,我就加了一句话,他就成功了,我在 for range 里面引入了一个中间变量,每次迭代都重新声明一个变量 o,赋值后再将 v 的地址添加 n 切片中,这样成功解决了刚才的问题。

现在来解释一下原因:在 for range 中,变量 v 是用来保存迭代切片所得的值,因为 v 只被声明了一次,每次迭代的值都是赋值给 v,该变量的内存地址始终未变,这样讲他的地址追加到新的切片中,该切片保存的都是同一个地址,这肯定无法达到预期效果的。这里还需要注意一点,变量 v 的地址也并不是指向原来切片 u[2] 的,因我在使用 range 迭代的时候,变量 v 的数据是切片的拷贝数据,所以直接 copy 了结构体数据。

上面的问题还有一种解决方法,直接引用数据的内存,这个方法比较好,不需要开辟新的内存空间,看代码:

......略
for k,_ := range u{
		n = append(n, &u[k])
	}
......略

2. 迭代修改变量问题

还是刚才的例子,我们做一点改动,现在我们要对切片中保存的每个用户的年龄进行修改,因为我们都是永远 18 岁,嘎嘎嘎~~~。

package main

import (
	"fmt"
)

type user struct {
	name string
	age uint64
}

func main()  {
	u := []user{
		{"asong",23},
		{"song",19},
		{"asong2020",18},
	}
	for _,v := range u{
		if v.age != 18{
			v.age = 20
		}
	}
	fmt.Println(u)
}

来看一下运行结果:

[{asong 23} {song 19} {asong2020 18}]

哎呀,怎么回事。怎么没有更改呢。其实道理都是一样,还记得,我在上文说的一个知识点嘛。对,就是这个,想起来了吧。v 变量是拷贝切片中的数据,修改拷贝数据怎么会对原切片有影响呢,还是这个问题,copy 这个知识点很重要,一不注意,就会出现问题。知道问题了,我们现在来把这个问题解决吧。

package main

import (
	"fmt"
)

type user struct {
	name string
	age uint64
}

func main()  {
	u := []user{
		{"asong",23},
		{"song",19},
		{"asong2020",18},
	}
	for k,v := range u{
		if v.age != 18{
			u[k].age = 18
		}
	}
	fmt.Println(u)
}

可以看到,我们直接对切片的值进行修改,这样就修改成功了。所以这里还是要注意一下的,防止以后出现 bug

3. 是否会造成死循环

来看一段代码:

func main() {
	v := []int{1, 2, 3}
	for i := range v {
		v = append(v, i)
	}
}

这一段代码会造成死循环吗?答案:当然不会,前面都说了 range 会对切片做拷贝,新增的数据并不在拷贝内容中,并不会发生死循环。这种题一般会在面试中问,可以留意下的。