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 · 3 天前 · 3217 次点击

    背景:这个地方的 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))
    }
    

    感觉被绕晕了

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

    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  
       3 天前
    不会 go ,盲猜一下。参数有引用,具名参数返回时先运行 defer 。不使用具名应该是直接返回了再 defer ?
    zhengfan2016
        7
    zhengfan2016  
    OP
       3 天前
    @maxwellz 对的,我就想问这个问题,为什么不设置名称 defer 就改不动呢
    R136a1
        8
    R136a1  
       3 天前
    值传递和引用传递的区别?
    ninjashixuan
        9
    ninjashixuan  
       3 天前
    想学这类边界技巧可以关注 go101 的作者。
    NessajCN
        10
    NessajCN  
       3 天前
    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  
       3 天前
    谁要在开发中这么写代码 小心被打
    zhengfan2016
        12
    zhengfan2016  
    OP
       3 天前
    @sardina 哈哈,我感觉你可以整理一个 awesome golang 容易挨打的代码片段,让新手村的 xdm 学习
    vincentWdp
        13
    vincentWdp  
       3 天前
    ```
    func foo(n int) (t int) {
    t = n
    ```
    换成
    ```
    func foo(n int) int {
    t := n
    ```
    ChrisFreeMan
        14
    ChrisFreeMan  
       3 天前
    @lesismal 看到你也不会我就放心了
    zhengfan2016
        15
    zhengfan2016  
    OP
       3 天前
    @NessajCN 原来如此
    sardina
        16
    sardina  
       3 天前   ❤️ 1
    第一个例子 return 是先把返回值存到临时变量里,然后 defer 再修改也改不到临时变量
    第一个例子因为返回值有命名,所以 return 是把返回值存到这个命名里里,然后 defer 就可以修改了
    总的来说就是 return 先设置返回值 然后再执行 defer ,然后函数返回
    https://www.cnblogs.com/saryli/p/11371912.html 可以看这个
    peteretep
        17
    peteretep  
       3 天前
    后端仔都不这么写的。不要起步就走犄角旮旯了。没有实际意义的。

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

    defer 只用来释放资源,其他使用正常的程序算法解决。
    maxwellz
        18
    maxwellz  
       3 天前
    @zhengfan2016 #7 貌似和 defer 的特性有关系了,这块太久没看了,忘了
    Liv1Dad
        19
    Liv1Dad  
       3 天前
    很简单啊,defer 放到 一个栈里面。
    defer func() { t+=3}() 这个匿名函数 放入到栈中, 等 function 结束时运行。
    第一个 函数返回 t, 函数结束后继续执行了 t+=3 。
    第二个 函数返回 t 的值,数据结束后继续执行了 t+=3, 此时的 t 和函数返回结果 没有关系。
    Liv1Dad
        20
    Liv1Dad  
       3 天前
    @wunonglin #2 我代码经常写 defer ,比如打开文件需要 close, 或者回收 chan, 还有数据库事物结束。
    coderlxm
        21
    coderlxm  
       3 天前 via Android
    看来还是要多问啊,之前我这里也有疑惑但是实际没有这种写法就没管了。
    mightybruce
        22
    mightybruce  
       3 天前
    大家其实都是猜测, 要真深入,直接让 go 生成编译的汇编,直接查看汇编代码就好
    https://gocompiler.shizhz.me/10.-golang-bian-yi-qi-han-shu-bian-yi-ji-dao-chu/10.2.1-ssa
    hugozach
        23
    hugozach  
       3 天前
    使用具名返回值时,defer 修改的是返回值本身,因此能在返回之前修改返回值。
    如果没有具名返回值,defer 修改的是函数中的局部变量,和返回值是两回事。返回值是在 defer 执行之后才被确定的。
    szdubinbin
        24
    szdubinbin  
       3 天前
    我第一眼看到就觉得,这不是 useEffect 第二个返回函数的意思吗,你在这里搞有副作用(effect)的事情显然不妥吧,当然具体逻辑跟 19 楼意思差不多。
    docxs
        25
    docxs  
       3 天前   ❤️ 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  
       3 天前
    xausky
        27
    xausky  
       3 天前   ❤️ 2
    这 TM 纯纯八股文题目,实际你用 go 10 年也写不出这种情况的代码。
    lovelylain
        28
    lovelylain  
       3 天前 via Android
    你觉得别扭是因为这两个例子只是为了出题,等你遇到了合适的使用场景,就会发现 defer 的设计非常合理。例如具名返回,考虑这种场景,你要在一个处理函数里进行很多处理,最终根据是否 return err 封装回包,具名返回可以让你在 defer 里拿到的是 return 的值;还有 defer 的参数是在 defer 的时候就计算的,这样就不用担心后面对相应变量重新赋值引发的问题。
    iseki
        29
    iseki  
       3 天前 via Android
    具名返回值的这个特性可以写
    defer func(){ if e!=nil{e=...}}
    这样的代码,算作是没有 try...catch 和 stacktrace 的一种补偿吧。
    iseki
        30
    iseki  
       3 天前 via Android   ❤️ 2
    不要动不动去看反汇编,实际上发生了什么都能想象到,汇编只是编译器按照语言规范编译的结果而已。真正值得去探索下的是为什么语言规范要这么写,为什么语言要这么设计。
    zhengfan2016
        31
    zhengfan2016  
    OP
       3 天前
    @iseki #29 难道 golang 的 recover 不是对标 js 的 try...catch 的吗,golang 用 panic 抛出,js 用 throw 抛出,感觉 defer 更像是对标 js 的 try...finally
    quantal
        32
    quantal  
       2 天前
    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  
       2 天前
    纯纯八股文,现实中这样写出现了 bug 扣你绩效
    PTLin
        34
    PTLin  
       2 天前
    命名返回值是比 if err = nil 错误处理更蠢的设计
    LieEar
        35
    LieEar  
       2 天前
    go 也开始 java 八股文化了
    lasuar
        36
    lasuar  
       2 天前
    具名返回值定义了一个变量,既然是变量,就可以被修改。没有定义变量,就以 return 值为准。
    fds
        37
    fds  
       2 天前
    @zhengfan2016 印象中似乎只有 python 推荐把 try catch 作为常规手段,用来让主体逻辑更简单。java 可能用的也不少? js 忘了。
    Go 如果 panic 应该直接退出进程的。留个 recover 只是以防万一,比如避免第三方代码崩溃什么的,正常情况还是应该中断,然后查原因的。如果是可以处理的错误,还是应该正常返回 err ,这样更快。
    defer 主要是解决 C 语言中 open() close() 需要配对使用的问题,没有 defer 可能 close() 得写好多次,很不方便,还容易遗漏。总体来讲 Go 是对 C 语言的补全,跟很多面向对象的语言思路不一样。
    zhengfan2016
        38
    zhengfan2016  
    OP
       2 天前
    @fds 对的,这个还是看情况,像 js 有些第三方库比如 zod 之类如果用户输入的值和校验类型不一致,会 throw ,有些 jwt 校验库 jwt 不合法也是会 throw ,这种肯定是希望接口返回 400 而不是 nodejs 进程直接退出了。

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

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

    2.函数有命名返回值(第一个例子),return 时,会先计算返回值,然后将返回值赋值给命名返回值,defer 修改命名返回值,会影响最终返回值
    kuanat
        40
    kuanat  
       2 天前
    我在过去几年的代码库里检索了一下,只找到了一种涉及到 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  
       2 天前   ❤️ 1
    你只要记住当你声明 (t int) 作为返回值时,t 是一个与返回值绑定的变量就行了,没有具名返回值的话,t 就没和返回值绑定,在返回的那一刻就确定值了。
    lxdlam
        42
    lxdlam  
       2 天前
    这个问题非常得“巧妙”,因为他混合了三个东西,把这个结果拖向了一个“记住就行”的深渊。

    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  
       2 天前
    别学了,这种代码再学下去脑子该坏了。
    duzhuo
        44
    duzhuo  
       2 天前
    人肉编译器哈哈
    onnethy
        45
    onnethy  
       1 天前
    说实话啊 这两段代码直接扔到 ds 里,给你讲的明明白白了
    虽然我看也你的代码 也绕得很 但是 ds 倒是给我讲明白了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1115 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 18:00 · PVG 02:00 · LAX 11:00 · JFK 14:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.