
Go 语言中,defer 语句中可以修改函数返回变量,导致返回值被修改。请看下面各种情况:
package main import "fmt" // 返回不受 defer 影响,返回 0 func f0() int { var i int defer func() { i++ }() return i } // 返回被 defer 修改后的值 1 func f1() (i int) { defer func() { i++ }() return } // 返回引用类型受 defer 影响,返回 [2] func f2() []int { var i = []int{1} defer func() { i[0]++ }() return i } // 指明返回变量 a ,但实际返回的还是 i 。经过 defer 修改后返回 3 func f3() (i int) { i = 100 // 在 return 时被重新赋值 a := 2 defer func() { i++ }() return a } // i 在返回前赋值为 3 但不返回,经过 defer 修改,返回 4 func f4() (i int) { defer func() { i++ }() return 3 } func main() { fmt.Println(f0(), f1(), f2(), f3(), f4()) // 输出:0 1 [2] 3 4 } 站内有篇对这一现象说明的文章,经过实际测试( Go 1.22 )得到下面理解:
return 时,这个隐藏变量被赋予 i 的字面量值。defer 语句中修改 i 的值不会影响到隐藏返回变量,所以最终返回 0。当然,如果 i 的类型为引用型(例如切片),那么赋值给隐藏返回变量时,是引用传递,defer 语句中的修改依然会体现到返回值上。i 赋值,如果 return 后带有值(或变量)则赋给 i;然后执行 defer 语句,里面可能修改 i 的值;最后将 i 的最终值返回。经过上面解释,正常 return 语句和裸 return 语句在逻辑上达成了一致。
这里不讨论为什么具名返回值被设计成这样的,我的疑惑是:
如果没有使用场景,我便直接将其理解为「陷阱」。就同 for 循环初始化变量只初始化一次一样,小心别用,等待官方改进就是了。
经验尚浅,还望大佬们不吝赐教!
1 nagisaushio Aug 26, 2024 via Android 场景:defer 中捕捉到 panic ,转成 err 返回 |
2 maocat Aug 26, 2024 via iPhone 代码出现了 panic ,我需要转成 err 返回 |
3 assassing OP @nagisaushio #1 准,算一例 |
4 povsister Aug 26, 2024 一种变相的 catch all 操作 除了 panic 转 err 外,业务上常见对于数据进行返回前检查 将检查条件写入 defer 内,这样可以在一大坨可能好几个月后看不懂的逻辑里少写点 return 赋值。 |
6 rekulas Aug 27, 2024 算特性吧, 会这样写的大多应该了解这样做会产生什么影响,不至于引起意外 bug 我很喜欢这特性,当做函数的析构逻辑来用,例如某些复杂方法内部可能包含多个判断,可能会涉及提前返回,不同的返回可能关联了不同的退出逻辑,我就可以在 defer 里统一处理,而不用每个 return 都去调用或者判断 |
7 assassing OP @rekulas #6 请问怎么理解「不同的返回可能关联了不同的退出逻辑」?这是使用 defer 修改返回值的关键。我理解的这种情况,需要后续单独使用一个函数来处理返回值,除非在 defer 中处理能获得相当大的便利性 |