V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
assassing
V2EX  ›  程序员

Go 语言中延迟语句修改返回值算陷阱还是特性?

  •  
  •   assassing ·
    hxz393 · 25 天前 · 1713 次点击

    0x00 现象

    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
    }
    

    0x01 解释

    站内有篇对这一现象说明的文章,经过实际测试( Go 1.22 )得到下面理解:

    • 正常返回(匿名返回值):函数内部会初始化一个隐藏局部变量储存返回值,在运行到 return 时,这个隐藏变量被赋予 i 的字面量值。defer 语句中修改 i 的值不会影响到隐藏返回变量,所以最终返回 0。当然,如果 i 的类型为引用型(例如切片),那么赋值给隐藏返回变量时,是引用传递,defer 语句中的修改依然会体现到返回值上。
    • 具名返回(命名返回值):要把返回分为三步分析。先给具名返回变量 i 赋值,如果 return 后带有值(或变量)则赋给 i;然后执行 defer 语句,里面可能修改 i 的值;最后将 i 的最终值返回。

    经过上面解释,正常 return 语句和裸 return 语句在逻辑上达成了一致。

    0x02 疑惑

    这里不讨论为什么具名返回值被设计成这样的,我的疑惑是:

    • 在延迟语句中,修改返回值的使用场景是什么?
    • 承接上一问,如果有使用场景,那么和具名返回配合的使用场景是什么?

    如果没有使用场景,我便直接将其理解为「陷阱」。就同 for 循环初始化变量只初始化一次一样,小心别用,等待官方改进就是了。

    经验尚浅,还望大佬们不吝赐教!

    9 条回复    2024-08-28 10:38:51 +08:00
    nagisaushio
        1
    nagisaushio  
       25 天前 via Android   ❤️ 1
    场景:defer 中捕捉到 panic ,转成 err 返回
    maocat
        2
    maocat  
       25 天前 via iPhone
    代码出现了 panic ,我需要转成 err 返回
    assassing
        3
    assassing  
    OP
       25 天前
    @nagisaushio #1 准,算一例
    povsister
        4
    povsister  
       25 天前
    一种变相的 catch all 操作
    除了 panic 转 err 外,业务上常见对于数据进行返回前检查
    将检查条件写入 defer 内,这样可以在一大坨可能好几个月后看不懂的逻辑里少写点 return 赋值。
    assassing
        5
    assassing  
    OP
       25 天前
    @povsister #4 可是可以,但那样就不好写测试代码了
    rekulas
        6
    rekulas  
       24 天前   ❤️ 1
    算特性吧, 会这样写的大多应该了解这样做会产生什么影响,不至于引起意外 bug
    我很喜欢这特性,当做函数的析构逻辑来用,例如某些复杂方法内部可能包含多个判断,可能会涉及提前返回,不同的返回可能关联了不同的退出逻辑,我就可以在 defer 里统一处理,而不用每个 return 都去调用或者判断
    assassing
        7
    assassing  
    OP
       23 天前
    @rekulas #6 请问怎么理解「不同的返回可能关联了不同的退出逻辑」?这是使用 defer 修改返回值的关键。我理解的这种情况,需要后续单独使用一个函数来处理返回值,除非在 defer 中处理能获得相当大的便利性
    rekulas
        8
    rekulas  
       23 天前   ❤️ 1
    @assassing 后续单独使用一个函数来处理返回值
    自然也是可以的,但如果你的逻辑依赖的数据是函数内部变量,那你可以选择定义内部函数然后每个退出都调用一次,总归没有 defer 方便,而且可能遗漏导致 bug ,另外有些场景下可能需要对返回值进行二次处理,用 defer 也更方便,不容易出 bug
    举个例子,一个典型场景函数内部开启事务,在 defer 中 commit 就可以避免出现事务未提交阻塞数据库的情况,现在很多 orm 也用这个原理封装了事务函数,尽最大可能降低了 bug 产生率
    assassing
        9
    assassing  
    OP
       23 天前
    @rekulas #8 理解了,谢谢!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2428 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 36ms · UTC 01:08 · PVG 09:08 · LAX 18:08 · JFK 21:08
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.