Go语言中for-range使用踩坑指南

这里写目录标题

  • Go语言中for-range使用踩坑指南
    • 1. 指针数据坑
    • 2. 迭代修改变量问题
    • 3. 是否会造成死循环
      • 你不知道的`range`用法
        • `delete`
        • add

Go语言中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。我们预期应该是显示uslice的内容,但是运行结果如下:

[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. 迭代修改变量问题

还是刚才的例子,我们做一点改动,现在我们要对切片中保存的每个用户的年龄进行修改。

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会对切片做拷贝,新增的数据并不在拷贝内容中,并不会发生死循环。这种题一般会在面试中问,可以留意下的。

你不知道的range用法

delete

没看错,删除,在range迭代时,可以删除map中的数据,第一次见到这么使用的,我刚听到确实不太相信,所以我就去查了一下官方文档,确实有这个写法:

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

看看官方的解释:

The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If map entries that have not yet been reached are removed during iteration, the corresponding iteration values will not be produced. If map entries are created during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and from one iteration to the next. If the map is nil, the number of iterations is 0.

翻译:
未指定`map`的迭代顺序,并且不能保证每次迭代之间都相同。 如果在迭代过程中删除了尚未到达的映射条目,则不会生成相应的迭代值。 如果映射条目是在迭代过程中创建的,则该条目可能在迭代过程中产生或可以被跳过。 对于创建的每个条目以及从一个迭代到下一个迭代,选择可能有所不同。 如果映射为nil,则迭代次数为0。

看这个代码:

func main()  {
	d := map[string]string{
		"jt": "帅",
		"jj": "太帅了",
	}
	for k := range d{
		if k == "jt"{
			delete(d,k)
		}
	}
	fmt.Println(d)
}

# 运行结果
map[jj:太帅了]
add

上面是删除,那肯定会有新增呀,直接看代码吧。

func main()  {
	d := map[string]string{
		"jt": "帅",
		"jj": "太帅了",
	}
	for k,v := range d{
		d[v] = k
		fmt.Println(d)
	}
}

这里我把打印放到了range里,你们思考一下,新增的元素,在遍历时能够遍历到呢。我们来验证一下。

func main()  {
	var addTomap = func() {
		var t = map[string]string{
			"j": "太帅",
			"t": "好帅",
			"jj": "非常帅",
		}
		for k := range t {
			t["j2022"] = "真帅"
			fmt.Printf("%s%s ", k, t[k])
		}
	}
	for i := 0; i < 10; i++ {
		addTomap()
		fmt.Println()
	}
}

运行结果:

j太帅 t好帅 jj非常帅
j太帅 t好帅 jj非常帅 j2022真帅 
jj非常帅 j2022真帅 j太帅 t好帅 
j太帅 t好帅 jj非常帅 j2022真帅 
t好帅 jj非常帅 j2022真帅 j太帅 
t好帅 jj非常帅 j2022真帅 j太帅 
j太帅 t好帅 jj非常帅 j2022真帅 
j太帅 t好帅 jj非常帅 j2022真帅 
j太帅 t好帅 jj非常帅 j2022真帅
j太帅 t好帅 jj非常帅

从运行结果,我们可以看出来,每一次的结果并不是确定的。这是为什么呢?这就来揭秘,map内部实现是一个链式hash表,为了保证无顺序,初始化时会随机一个遍历开始的位置,所以新增的元素被遍历到就变的不确定了,同样删除也是一个道理,但是删除元素后边就不会出现,所以一定不会被遍历到。

你可能感兴趣的:(Golang复习,golang,开发语言,后端)