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
求大佬赐教
1
gerorim 229 天前 via iPhone
在第一个 for 中,u 是 users1 数组中每个元素的副本。当使用 defer 时,scheduler 对 GetName()的调用在函数末尾执行(即 main())。重要的是,defer 捕获变量 u 本身,而不是 u 在每次迭代时指向的值。因为 u 是一个结构(不是指针),它会被循环的每次迭代覆盖,到 main()退出时,u 为循环的最后一个值,即{Name: "c"}。因此,GetName ()打印“c”三次。
|
2
gerorim 229 天前 via iPhone
第二个 for ,u 是一个指针,直接指向用户 2 切片中的每个元素。同样,defer 捕获 u ,但在这里,每个 u 都是一个不同的指针,指向不同的地址。循环分别捕获每个指针,当延迟执行对 GetName()的调用时,每个指针都指向不同的用户结构。此外,由于延迟执行 LIFO ( Last In ,First Out )顺序的函数,所以应该看到“z”、“y”、“x”(循环顺序的反转),而不是楼主所要的“z”、“z”、“z”。
|
3
twl007 229 天前 2
Fixing For Loops in Go 1.22
https://go.dev/blog/loopvar-preview |
4
twl007 229 天前 via iPhone 1
第一个行为在 1.22 修复了
在 go mid 里面的的版本小于 1.22 的时候会继续保持以前的行为 在版本大于 1.22 的时候会修正这个问题 |
5
lance6716 229 天前 via Android 1
因为你的 GetName 定义在 *Users 上,当变量是类型 Users 会有一个隐含的取地址,再加上旧版本 for loop 用的是同一个地址,就会变成 ccc
|
6
povsister 229 天前 via iPhone 3
一楼说的属于是牛头不对马嘴了… 这两个 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() } 这个输出什么呢?(笑 |
7
lance6716 229 天前 via Android
第二个 for loop:defer 并不是闭包,所以跟你说的“指向最后一个”没关系。你是直接被第一个 for loop 搞迷糊了,误以为自己掌握了某个坑能解释这个奇怪行为,其实只是瞎猫碰上死耗子
建议升级到 go1.22 直接避免踩坑 |
8
main1234 OP @povsister 我有点懵了,对于第二个 forrange 来说,defer 相当于压栈,底层是个链表,u 的地址是同一个,链表中 u 指向的地址难道不是最后一个结构体的地址????
|
9
main1234 OP @povsister 执行三次 defer ,相当于创建了 3 个链表节点,每个链表节点中 u 是同一个地址;当第一次执行 defer ,链表只有一个节点,u 指向结构体第一个元素;然后第二次执行 defer ,第一个链表节点 u 指向的元素不会变成第二个结构体元素么??
|
10
veightz 229 天前
也可以加一行
```golang u := u ``` |
11
leonshaw 229 天前 via Android
@main1234 注意 #6 说的“函数参数的 evaluate 发生在 defer 语句那一行”,包括对 receiver 的求值。第二个循环每次 u 的值不同,GetName 的 receiver 也就不同。第一个循环 u 的值不同但是地址相同,所以 GetName 的 receiver 也是相同的( 1.22 以后不是这样了)。
|
12
fkdtz 229 天前 9
我认为综合 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 的地址就能看到新版本确实每次循环都在发生变化,这样一来也就不存在上述问题了。 |
13
body007 229 天前 1
永远记住 go 所有赋值都是值传递,能解决任何疑问。只是引用类型的变量赋值的是引用地址而已,第一个 for ,每个 u 都是对象的值传递,等于复制了一个对象。第二个 for ,每个 u 都是对象地址的值传递,等于复制了对象地址,只是第二个 u 也能通过对象地址访问字段,这是 go 的语法糖,例如 (*a).name 简写为 a.name 这样。
当然最新的 go1.22 版本专门为 for range 特殊处理,这个版本两种 for 的 u 都是新对象,第二种也会是复制的新对象地址。 |
14
CzaOrz 229 天前
1.22 以前的 for range 是复用同一变量。也就是说系统帮你内置申请了一个临时变量,举个简单的例子:
``` var u Users for i := 0; i < len(users); i++ { u = users[i] // 以上等同于 for range // 以下等同于你的代码 defer u.GetName() } ``` |
15
kamier 229 天前
Go1.22 解君愁😂
|
16
0x90200 229 天前
第一个 u 复制了 range []Users{{"a"}, {"b"}, {"c"}} 的值, 第二个 u 复制 []*Users{{"a"}, {"b"}, {"c"}} 的地址
|
17
dyllen 229 天前
@main1234 按照#6 说的,循环 1 u 是 Users ,执行到 defer 那行时拿到的是 u 本身的内存地址也就是&u ,每次循环都会被覆盖。循环 2 u 是*Users ,执行到 defer 那行拿的是*Users 的地址,每次 defer 都是不同的,下一次也就不会覆盖上一次的值了。
|
18
zzhaolei 228 天前
#6 楼说的对。但是建议升级 go1.22 。官方已经改了,强行理解这个东西用处也不大
|