分享
  1. 首页
  2. 文章

Golang 之 我被 for-range 循环进去了

大叔说码 · · 629 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

看个例子

在我们平时的代码场景中,常常需要改变切片中某个元素的值,先来看一下常见的代码实现方式:

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)
}
func main() {
 test1()
 test2()
 test3()
}

非常简单,test1() 中的修改并未对原数据产生影响,而 test2() 和 test3() 中的修改真正改变了原数据。我们看一下打印的结果:

[1 2 3 4]
[2 3 4 5]
[2 3 4 5]

最终输出也是跟我们预想的一致。

主要原因是因为:

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

为什么会出现这种情况呢?我们去了解一下for - range 原理

for - range 原理

for range的实现

// 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 的编译方式如下:

// 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
// }

具体代码细节可查看 https://github.com/golang/gofrontend/blob/e387439bfd24d5e142874b8e68e7039f74c744d7/go/statements.cc#L5384 源代码

从上面的源码我们可以看到,针对slice,for range 做了一下事情:

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

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

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

package main
import "fmt"
func test4() {
 s := []int{0, 1, 2, 3}
 m := make(map[int]*int)
 for index, value := range s {
 m[index] = &value
 }
 
 printMap(m)
}
func printtMap(m map[int]*int) {
 for key, value := range m {
 fmt.Printf("map[%v]=%v\n", key, *value)
 }
}
func main() {
 test4()
}

打印输出:

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

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

如果我把 test4()方法 换成下面这种形式呢


func test4() {
 s := []int{0, 1, 2, 3}
 m := make(map[int]*int)
 for index, value := range s {
 valueCopy := value
 m[index] = &valueCopy
 }
 
 printtMap(m)
}

上面主要改变是:每次进入循环体,都声明一个新变量valueCopy,并把value赋值给它,最后把新变量valueCopy的地址存到 m 中

打印输出:

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

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

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

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

实际上上面的代码会输出:

Echo 大叔
Echo 大叔
Echo 大叔

原因见注释

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

package main
import (
 "fmt"
 "time"
)
func main() {
 str := []string{"I","am","Echo 大叔"}
 for _, v := range str{
 // 把外部的v值拷贝给函数内部的v
 go func(v string) {
 fmt.Println(v)
 }(v)
 }
 
 time.Sleep(3 * time.Second)
}

打印输出(打印顺序不一定一样):

I
am
Echo 大叔

对于slice

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

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)
 }
}

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

打印输出:

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

作业

最后,留一道题给大家

package main
import (
 "fmt"
)
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, "Echo", "大叔", []int{1,2, 3, 4, 5}}
 fmt.Println(test)
 test.removeFriend(4)
 fmt.Println(test) 
}

最终会打印输出:

{0 Echo 大叔 [1 2 3 4 5]}
{0 Echo 大叔 [1 2 3 5 5]}

大家知道其中原因吗?欢迎评论交流〜

关注公众号 「大叔说码」,获取更多干货,下期见〜


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:大叔说码

查看原文:Golang 之 我被 for-range 循环进去了

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
629 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏