V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
zhengfan2016
V2EX  ›  Go 编程语言

前端仔有点学不明白 golang 的 defer

  •  
  •   zhengfan2016 · 12 天前 · 3533 次点击

    背景:这个地方的 test-1 题 https://golang.dbwu.tech/traps/defer_exam/

    如下 test-1 题,使用具名返回值,defer 就能修改 t 的值

    package main
    
    func foo(n int) (t int) {
    	t = n
    	defer func() {
    		t += 3
    	}()
    	return t
    }
    
    func main() {
    	println(foo(1))
    }
    

    但是我不使用具名,就算我把 t 移到最外层的作用域,defer 也改变不了 t 的值,我试着不在 defer 作用域内,就可以修改

    package main
    
    var t int
    
    func foo(n int) int {
    	t = n
    	defer func() {
    		t += 3
    	}()
    	return t
    }
    
    func main() {
    	println(foo(1))
    }
    

    感觉被绕晕了

    46 条回复    2025-04-08 21:30:05 +08:00
    lesismal
        1
    lesismal  
       12 天前   ❤️ 14
    不知道谁带头搞的这些题啊,我一个都不会做、只能运行跑结果来看才知道答案。
    但是我从来都不会这样用 defer 导致这种问题啊,搞这些题的人是吃得太饱了吗!
    wunonglin
        2
    wunonglin  
       12 天前
    虽然我知道有些基础应该会,不过我写了 go3 、4 年确实没碰到过这种场景。我麻了 hhhhh
    rahuahua
        3
    rahuahua  
       12 天前
    下面这个 defer 也是能修改 t 的值,只是返回值已经拷贝了 t 的值,不受影响了
    maxwellz
        4
    maxwellz  
       12 天前
    返回值如果没有设置名称,defer 中的值不会改变返回值
    kcross
        5
    kcross  
       12 天前
    给你看个好玩的,你试试这个

    package main

    var t int

    func foo(n int) (t int) {
    t = n
    defer func() {
    t = t+ 3
    }()
    return
    }

    func main() {
    println(foo(1))
    }
    uion
        6
    uion  
       12 天前
    不会 go ,盲猜一下。参数有引用,具名参数返回时先运行 defer 。不使用具名应该是直接返回了再 defer ?
    zhengfan2016
        7
    zhengfan2016  
    OP
       12 天前
    @maxwellz 对的,我就想问这个问题,为什么不设置名称 defer 就改不动呢
    R136a1
        8
    R136a1  
       12 天前
    值传递和引用传递的区别?
    ninjashixuan
        9
    ninjashixuan  
       12 天前
    想学这类边界技巧可以关注 go101 的作者。
    NessajCN
        10
    NessajCN  
       12 天前
    https://go.dev/blog/defer-panic-and-recover

    "3. Deferred functions may read and assign to the returning function's named return values."

    纯粹就是 named return 特性,死记就好了
    sardina
        11
    sardina  
       12 天前
    谁要在开发中这么写代码 小心被打
    zhengfan2016
        12
    zhengfan2016  
    OP
       12 天前
    @sardina 哈哈,我感觉你可以整理一个 awesome golang 容易挨打的代码片段,让新手村的 xdm 学习
    vincentWdp
        13
    vincentWdp  
       12 天前
    ```
    func foo(n int) (t int) {
    t = n
    ```
    换成
    ```
    func foo(n int) int {
    t := n
    ```
    ChrisFreeMan
        14
    ChrisFreeMan  
       12 天前
    @lesismal 看到你也不会我就放心了
    zhengfan2016
        15
    zhengfan2016  
    OP
       12 天前
    @NessajCN 原来如此
    sardina
        16
    sardina  
       12 天前   ❤️ 1
    第一个例子 return 是先把返回值存到临时变量里,然后 defer 再修改也改不到临时变量
    第一个例子因为返回值有命名,所以 return 是把返回值存到这个命名里里,然后 defer 就可以修改了
    总的来说就是 return 先设置返回值 然后再执行 defer ,然后函数返回
    https://www.cnblogs.com/saryli/p/11371912.html 可以看这个
    peteretep
        17
    peteretep  
       12 天前
    后端仔都不这么写的。不要起步就走犄角旮旯了。没有实际意义的。

    这个和 c++考试 i++++ 、 ++i++ 的题目有什么区别吗?

    defer 只用来释放资源,其他使用正常的程序算法解决。
    maxwellz
        18
    maxwellz  
       12 天前
    @zhengfan2016 #7 貌似和 defer 的特性有关系了,这块太久没看了,忘了
    Liv1Dad
        19
    Liv1Dad  
       12 天前
    很简单啊,defer 放到 一个栈里面。
    defer func() { t+=3}() 这个匿名函数 放入到栈中, 等 function 结束时运行。
    第一个 函数返回 t, 函数结束后继续执行了 t+=3 。
    第二个 函数返回 t 的值,数据结束后继续执行了 t+=3, 此时的 t 和函数返回结果 没有关系。
    Liv1Dad
        20
    Liv1Dad  
       12 天前
    @wunonglin #2 我代码经常写 defer ,比如打开文件需要 close, 或者回收 chan, 还有数据库事物结束。
    coderlxm
        21
    coderlxm  
       12 天前 via Android
    看来还是要多问啊,之前我这里也有疑惑但是实际没有这种写法就没管了。
    mightybruce
        22
    mightybruce  
       12 天前
    大家其实都是猜测, 要真深入,直接让 go 生成编译的汇编,直接查看汇编代码就好
    https://gocompiler.shizhz.me/10.-golang-bian-yi-qi-han-shu-bian-yi-ji-dao-chu/10.2.1-ssa
    hugozach
        23
    hugozach  
       12 天前
    使用具名返回值时,defer 修改的是返回值本身,因此能在返回之前修改返回值。
    如果没有具名返回值,defer 修改的是函数中的局部变量,和返回值是两回事。返回值是在 defer 执行之后才被确定的。
    szdubinbin
        24
    szdubinbin  
       12 天前
    我第一眼看到就觉得,这不是 useEffect 第二个返回函数的意思吗,你在这里搞有副作用(effect)的事情显然不妥吧,当然具体逻辑跟 19 楼意思差不多。
    docxs
        25
    docxs  
       12 天前   ❤️ 3
    直接看下汇编就好了:
    test-1 具名返回,0x8(SP)就是 t ,defer 里也会修改这个地址的值,最后 MOVQ 0x8(SP), AX 再给返回值,另外 return 的时候有没有 t 都一样


    test-2 不具名,先是把 t 的值 0x10(SP)给了返回暂存值 0x8(SP),然后执行 defer ,执行完再把暂存值 0x8(SP)给到 AX 做返回值,在 defer 里改的是 0x10(SP),并未改到 0x8(SP),所以返回值是最初的 t


    这种具名返回也一样
    docxs
        26
    docxs  
       12 天前
    xausky
        27
    xausky  
       12 天前   ❤️ 2
    这 TM 纯纯八股文题目,实际你用 go 10 年也写不出这种情况的代码。
    lovelylain
        28
    lovelylain  
       12 天前 via Android
    你觉得别扭是因为这两个例子只是为了出题,等你遇到了合适的使用场景,就会发现 defer 的设计非常合理。例如具名返回,考虑这种场景,你要在一个处理函数里进行很多处理,最终根据是否 return err 封装回包,具名返回可以让你在 defer 里拿到的是 return 的值;还有 defer 的参数是在 defer 的时候就计算的,这样就不用担心后面对相应变量重新赋值引发的问题。
    iseki
        29
    iseki  
       12 天前 via Android
    具名返回值的这个特性可以写
    defer func(){ if e!=nil{e=...}}
    这样的代码,算作是没有 try...catch 和 stacktrace 的一种补偿吧。
    iseki
        30
    iseki  
       12 天前 via Android   ❤️ 2
    不要动不动去看反汇编,实际上发生了什么都能想象到,汇编只是编译器按照语言规范编译的结果而已。真正值得去探索下的是为什么语言规范要这么写,为什么语言要这么设计。
    zhengfan2016
        31
    zhengfan2016  
    OP
       12 天前
    @iseki #29 难道 golang 的 recover 不是对标 js 的 try...catch 的吗,golang 用 panic 抛出,js 用 throw 抛出,感觉 defer 更像是对标 js 的 try...finally
    quantal
        32
    quantal  
       12 天前
    defer 的用法总结了三条规则
    #### defer 不能修改非具名返回值,可以修改具名返回值,具名返回值进入函数时为 0
    #### defer 传入的参数定义时确定,执行不与定义同步进行
    #### defer 执行时机:return 执行后,函数真正的返回前执行,LIFO

    func foo() (t int) {
    defer func(n int) {
    println(n)
    println(t)
    t = 9
    }(t)
    t = 1
    return 2
    }

    func main() {
    println("result:", foo())

    结果是:
    0
    2
    result: 9
    Rehtt
        33
    Rehtt  
       11 天前
    纯纯八股文,现实中这样写出现了 bug 扣你绩效
    PTLin
        34
    PTLin  
       11 天前
    命名返回值是比 if err = nil 错误处理更蠢的设计
    LieEar
        35
    LieEar  
       11 天前
    go 也开始 java 八股文化了
    lasuar
        36
    lasuar  
       11 天前
    具名返回值定义了一个变量,既然是变量,就可以被修改。没有定义变量,就以 return 值为准。
    fds
        37
    fds  
       11 天前
    @zhengfan2016 印象中似乎只有 python 推荐把 try catch 作为常规手段,用来让主体逻辑更简单。java 可能用的也不少? js 忘了。
    Go 如果 panic 应该直接退出进程的。留个 recover 只是以防万一,比如避免第三方代码崩溃什么的,正常情况还是应该中断,然后查原因的。如果是可以处理的错误,还是应该正常返回 err ,这样更快。
    defer 主要是解决 C 语言中 open() close() 需要配对使用的问题,没有 defer 可能 close() 得写好多次,很不方便,还容易遗漏。总体来讲 Go 是对 C 语言的补全,跟很多面向对象的语言思路不一样。
    zhengfan2016
        38
    zhengfan2016  
    OP
       11 天前
    @fds 对的,这个还是看情况,像 js 有些第三方库比如 zod 之类如果用户输入的值和校验类型不一致,会 throw ,有些 jwt 校验库 jwt 不合法也是会 throw ,这种肯定是希望接口返回 400 而不是 nodejs 进程直接退出了。

    我不知道 golang 有没有库会在用户 post 接口输入不符合预期的时候直接 panic ,一般第三方库有 if err 肯定是用 err 的
    oom
        39
    oom  
       11 天前
    defer 在 return 之后,函数返回结束前执行,也就是处在两者之间

    1.函数无命名返回值(你的第 2 个例子),return 时,会先计算返回值,一旦计算完毕,defer 无论怎么修改,都不会影响最终返回值,但函数内部 defer 修改后的值是生效的,只是不会返回罢了

    2.函数有命名返回值(第一个例子),return 时,会先计算返回值,然后将返回值赋值给命名返回值,defer 修改命名返回值,会影响最终返回值
    kuanat
        40
    kuanat  
       11 天前
    我在过去几年的代码库里检索了一下,只找到了一种涉及到 defer 里面修改返回值操作的反例。严格来说,这个代码编写方式是 named return 的问题,而不是 defer 的问题。

    前面提到的 defer 里修改返回值的情况是:

    // fn 函数签名 fn() (err error)

    defer func() {
    err = writer.Close()
    }()

    这样就会覆盖掉原本 err ,所以还要新增变量特殊处理一下。

    defer func() {
    closeErr := writer.Close()
    if closeErr != nil {
    // 特殊处理
    }
    }()

    这样看起来就很蠢对吧,所以代码规范里就直接禁止了在 defer 里写逻辑。我确实想象不出来正常的业务代码里有什么一定非要在 defer 里处理的逻辑不可。个人的观点是,这个和三元逻辑操作符差不多,都是不适合工程上团队协作使用的。



    当然我这里的规范还有一条,interface 写 named return ,这样注释可以对应到参数名。

    我印象有个说法是 go 早期是手搓编译器,named return 能方便代码生成。其实我觉得这个特性除了支持 naked return 之外没什么意义,属于某种设计失误,但也有可能是我没理解到位。
    Feiir
        41
    Feiir  
       11 天前   ❤️ 1
    你只要记住当你声明 (t int) 作为返回值时,t 是一个与返回值绑定的变量就行了,没有具名返回值的话,t 就没和返回值绑定,在返回的那一刻就确定值了。
    lxdlam
        42
    lxdlam  
       11 天前
    这个问题非常得“巧妙”,因为他混合了三个东西,把这个结果拖向了一个“记住就行”的深渊。

    1. 返回值的处理:根据 Go 关于 [Return Value]( https://go.dev/ref/spec#Return_statements) 的规范,当你声明一个返回值的时候,你实际上是声明了一个临时对象,区别仅存在于这个返回对象是有名字还是没有名字的;
    2. Defer 的作用时机:根据 Go 关于 [Defer Statement]( https://go.dev/ref/spec#Defer_statements) 的规范,`defer` 的作用时机在 `return` 的所有 Value 都被计算且赋值完毕后,真实返回前执行;
    3. Go 对闭包的处理:根据 Go 关于 [Function literals]( https://go.dev/ref/spec#Function_literals) 的规范,闭包内捕获的自由变量会被共享;换句话说,你可以理解为闭包实际上捕获了外部变量的指针,对其的修改会同步到原始对象。

    花点时间理解上面三个规范会带来的代码作用。

    现在我们来分析代码:
    - 在 Case1 中,由于返回值被具名了,`return t` 实际上可以理解为 `t = t; return`,也就是仅仅重新赋值,之后执行的 `defer` 重新修改了 `t`,导致返回的值产生了变动。
    - 在 Case2 中,由于返回值匿名,假定返回值是一个隐藏变量 `tForReturn`,`return t` 实际上可以理解为 `tForReturn = t; return`,此时虽然你的 `defer` 修改了 `t`,但是由于返回的对象是 `tForReturn`,获取的返回值并没有发生变化,一切正常。当然,此时你再次在 `foo` 调用后查看 `t` 的值,它确实也会是 `4`,`defer` 的作用生效了。

    P.S. Go 的规范对这种行为没有明确的规定,上面的三个 spec 其实也只能说是“模糊”描述了作用原理,还是要观察编译器的实现实锤,这也是这门语言天天开天窗擦屁股的核心包袱之一;具名返回值这个特性本身也有很强的“拍脑袋”属性,它确实有用,但是没有有用到这个程度,结果反而引入了更多的混淆。
    DOGOOD
        43
    DOGOOD  
       11 天前
    别学了,这种代码再学下去脑子该坏了。
    duzhuo
        44
    duzhuo  
       11 天前
    人肉编译器哈哈
    onnethy
        45
    onnethy  
       10 天前
    说实话啊 这两段代码直接扔到 ds 里,给你讲的明明白白了
    虽然我看也你的代码 也绕得很 但是 ds 倒是给我讲明白了
    whinyStone
        46
    whinyStone  
       5 天前
    具名返回那种应该是在栈内存提前指定了返回值的位置,所以 defer 执行的时候可以改到那个位置;
    后面的返回值一般可能是寄存器传递的,汇编层面会是先算出来,然后执行 defer 的函数,然后弹栈返回;
    即使是栈内存传递,也会有一次复制,defer 仍改不到返回值,可以改一个大数组看看;
    堆内存就不一样了,在 foo 里声明一个切片并返回它,在 defer 里做修改,返回后的拿到的切片当然是修改后的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   866 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 22:00 · PVG 06:00 · LAX 15:00 · JFK 18:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.