golang 关于 forrange 的一些疑问

178 天前
 main1234
package main

import "fmt"

type Users struct {
	Name string
}

func (u *Users) GetName() {
	fmt.Println(u.Name)
}

func main() {
	users1 := []Users{{"a"}, {"b"}, {"c"}}
	for _, u := range users1 {
		defer u.GetName() //c  c  c
	}

	users2 := []*Users{{"x"}, {"y"}, {"z"}}
	for _, u := range users2 {
		defer u.GetName() //z x y
	}
}

我理解 forrange 的原理,无论第一个还是第二个 forrange ,u 都是同一个地址

对于第一个 forrange ,由于 u 是同一个地址,for 执行完毕后 u 地址指向最后一个{"c"},所以输出的都是 ccc

对于第二个 forrange ,我想不明白,for 循环完后,u 不是也是指向最后一个{"z"},那么输出的为啥不是 zzz

求大佬赐教

1765 次点击
所在节点    程序员
18 条回复
gerorim
178 天前
在第一个 for 中,u 是 users1 数组中每个元素的副本。当使用 defer 时,scheduler 对 GetName()的调用在函数末尾执行(即 main())。重要的是,defer 捕获变量 u 本身,而不是 u 在每次迭代时指向的值。因为 u 是一个结构(不是指针),它会被循环的每次迭代覆盖,到 main()退出时,u 为循环的最后一个值,即{Name: "c"}。因此,GetName ()打印“c”三次。
gerorim
178 天前
第二个 for ,u 是一个指针,直接指向用户 2 切片中的每个元素。同样,defer 捕获 u ,但在这里,每个 u 都是一个不同的指针,指向不同的地址。循环分别捕获每个指针,当延迟执行对 GetName()的调用时,每个指针都指向不同的用户结构。此外,由于延迟执行 LIFO ( Last In ,First Out )顺序的函数,所以应该看到“z”、“y”、“x”(循环顺序的反转),而不是楼主所要的“z”、“z”、“z”。
twl007
178 天前
Fixing For Loops in Go 1.22
https://go.dev/blog/loopvar-preview
twl007
178 天前
第一个行为在 1.22 修复了

在 go mid 里面的的版本小于 1.22 的时候会继续保持以前的行为 在版本大于 1.22 的时候会修正这个问题
lance6716
178 天前
因为你的 GetName 定义在 *Users 上,当变量是类型 Users 会有一个隐含的取地址,再加上旧版本 for loop 用的是同一个地址,就会变成 ccc
povsister
178 天前
一楼说的属于是牛头不对马嘴了… 这两个 for 没有本质区别,都是在不停对局部变量 u 进行赋值,op 疑惑的这个问题其实隐含了 3 个问题:
1. defer 的本质是什么?
2. go 编译器的自动 takeRef/deRef
3. func with receiver 到底是什么?

简单来说你可以认为,defer 会把函数压入栈中,而且函数参数的 evaluate 发生在 defer 语句那一行

所以 循环一的实际 defer 是这样的
defer GetName(&u)
循环二代实际 defer 是
defer GetName(u)

注意两个&的区别,再结合我说的,参数 evaluate 发生在 defer 语句书写时,这下 ,op 理解了否?

留个思考题。
defer func() { u.GetName() }
这个输出什么呢?(笑
lance6716
178 天前
第二个 for loop:defer 并不是闭包,所以跟你说的“指向最后一个”没关系。你是直接被第一个 for loop 搞迷糊了,误以为自己掌握了某个坑能解释这个奇怪行为,其实只是瞎猫碰上死耗子

建议升级到 go1.22 直接避免踩坑
main1234
178 天前
@povsister 我有点懵了,对于第二个 forrange 来说,defer 相当于压栈,底层是个链表,u 的地址是同一个,链表中 u 指向的地址难道不是最后一个结构体的地址????
main1234
178 天前
@povsister 执行三次 defer ,相当于创建了 3 个链表节点,每个链表节点中 u 是同一个地址;当第一次执行 defer ,链表只有一个节点,u 指向结构体第一个元素;然后第二次执行 defer ,第一个链表节点 u 指向的元素不会变成第二个结构体元素么??
veightz
178 天前
也可以加一行

```golang
u := u
```
leonshaw
178 天前
@main1234 注意 #6 说的“函数参数的 evaluate 发生在 defer 语句那一行”,包括对 receiver 的求值。第二个循环每次 u 的值不同,GetName 的 receiver 也就不同。第一个循环 u 的值不同但是地址相同,所以 GetName 的 receiver 也是相同的( 1.22 以后不是这样了)。
fkdtz
178 天前
我认为综合 4 、5 、6 楼的回复已经可以完整回答的楼主的问题了,不过貌似楼主有一点模糊,我根据自己的理解再具象化地补充一下,或许可以帮助楼主理解,也希望能和大家一起交流学习。
搞清楚下面 3 个 go 的特性可能有助于理解上面的代码发生了什么:
1.go 的自动引用和自动解引用; 2.defer 的求值时机和执行时机 3.for 循环变量只初始化一次之后一直在复用(go1.22 以前)

第一个特性自动引用和解引用指的是,如果一个方法是定义在指针类型上的,那么你可以通过该类型的值对象来调用方法。例如代码中 func (u *Users) GetName()定义在 *User 上,但却可以在 for 循环 users1[]Users 时通过 u.GetName() 调用。这里的完整写法其实应该是 (&u).GetName()。
自动解引用就是反过来,方法定义在值类型上,但允许你在指针类型上直接调用。

第二个特性 defer 的执行时机大家都懂,只是需要明确的是 defer 后面语句的求值时机,是在执行到这一行时就要求值,之后压栈。

第三个特性是循环变量 u 只初始化一次,即 u 的地址不会变(go1.22 之前),后面的循环是将新的列表元素值赋值给 u 。

现在回头看为什么第一个 for 打出 ccc ?
我们排除掉自动引用的干扰,还原完整写法,users1 的 for 循环中完整写法应该是 defer (&u).GetName(),执行到这里就得求值并压栈,压入的是 u 的地址,之后进入后面循环。
由于 u 只初始化一次,所以之后的循环中 u 的地址一直不变,只是在更新他的值,所以再次执行 defer (&u).GetName() 时压入的也还是 u 的地址,就这样一共循环 3 次,压了 3 次 u 的地址,最后 u 装的是 Users("c"),所以 GetName()打出三次 c 。

再看为什么第二个 for 打出 zyx ?
理解了第一个 for 也就能理解第二个 for 了,这次执行 defer u.GetName() 时不需要自动引用,因为 u 本身就是*User 类型,那么此时求值压栈,压入的就是 u 的值,注意 u 是指针,虽然 u 只初始化一次 u 的地址不变,但我们压入的并不是 u 的地址,而是 u 的值,u 的值是*User("x"),也就是 User("x")的地址,接着第二次循环,压入 User("y")地址,最后压入 User("z")地址,最终执行,得到结果就是 zyx 。

换一个角度思考,可以将 GetName 定义到 User 上,即 func (u Users) GetName(),其余代码不需要做改动,可以输出 zyxcba ,相当于 user1 循环不涉及自动引用,而 users2 循环中会自动解引用。

我想这也是很多 for 循环中如果嵌套函数或 goroutine 时,比较推荐用函数参数传值的方式而不是闭包的原因,因为 go 全都是值传递,这样就省掉了很多变量在函数内外生命周期的问题,心智负担轻了很多。

最后 4 楼提到在 go 1.22 版本做了修改,for 循环时循环变量已经改成每次循环都是一个全新变量了,这一点可以观察 u 的地址就能看到新版本确实每次循环都在发生变化,这样一来也就不存在上述问题了。
body007
178 天前
永远记住 go 所有赋值都是值传递,能解决任何疑问。只是引用类型的变量赋值的是引用地址而已,第一个 for ,每个 u 都是对象的值传递,等于复制了一个对象。第二个 for ,每个 u 都是对象地址的值传递,等于复制了对象地址,只是第二个 u 也能通过对象地址访问字段,这是 go 的语法糖,例如 (*a).name 简写为 a.name 这样。

当然最新的 go1.22 版本专门为 for range 特殊处理,这个版本两种 for 的 u 都是新对象,第二种也会是复制的新对象地址。
CzaOrz
178 天前
1.22 以前的 for range 是复用同一变量。也就是说系统帮你内置申请了一个临时变量,举个简单的例子:

```

var u Users
for i := 0; i < len(users); i++ {
u = users[i]

// 以上等同于 for range
// 以下等同于你的代码

defer u.GetName()
}

```
kamier
178 天前
Go1.22 解君愁😂
0x90200
178 天前
第一个 u 复制了 range []Users{{"a"}, {"b"}, {"c"}} 的值, 第二个 u 复制 []*Users{{"a"}, {"b"}, {"c"}} 的地址
dyllen
178 天前
@main1234 按照#6 说的,循环 1 u 是 Users ,执行到 defer 那行时拿到的是 u 本身的内存地址也就是&u ,每次循环都会被覆盖。循环 2 u 是*Users ,执行到 defer 那行拿的是*Users 的地址,每次 defer 都是不同的,下一次也就不会覆盖上一次的值了。
zzhaolei
178 天前
#6 楼说的对。但是建议升级 go1.22 。官方已经改了,强行理解这个东西用处也不大

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1040077

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX