go打印defer

中山桥一霸
  • 17

能解释一下为什么打印结果是这样吗
image.png
image.png

回复
阅读 1.6k
5 个回答

同为新手,个人理解:
首先defer后进先出,三二一的顺序应该没异议了。
其次根据defer注册要延迟执行的函数时该函数所有的参数都需要确定其值,
一二三按照代码顺序逐行注册,其中函数需求的参数也在注册时被赋值。
接着执行a++
随后RET之前开始按序执行defer三二一。
三:func(a int)内的a为形参,在注册时开辟了一块内存存放形参 a‘,其值为传入实参a=1。随后执行输出1
二:fmt.Println(a)同为函数,同上。
一:无参函数,注册时不开辟新内存。执行时先上寻找参数a(此时为2)

预期说这是一个关于 defer 的问题,不如说这是一个关于闭包的问题。

这个问题,如果你要想了解它最根本的原理,得去看defer相关的源码,相关代码在.../runtime/runtime2.go.../runtime/panic.go下。这里贴一下_defer结构体的源码:

// A _defer holds an entry on the list of deferred calls.
// If you add a field here, add code to clear it in freedefer and deferProcStack
// This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct
// and cmd/compile/internal/gc/ssa.go:(*state).call.
// Some defers will be allocated on the stack and some on the heap.
// All defers are logically part of the stack, so write barriers to
// initialize them are not required. All defers must be manually scanned,
// and for heap defers, marked.
type _defer struct {
    siz     int32 // includes both arguments and results
    started bool
    heap    bool
    // openDefer indicates that this _defer is for a frame with open-coded
    // defers. We have only one defer record for the entire frame (which may
    // currently have 0, 1, or more defers active).
    openDefer bool
    sp        uintptr  // sp at time of defer
    pc        uintptr  // pc at time of defer
    fn        *funcval // can be nil for open-coded defers
    _panic    *_panic  // panic that is running defer
    link      *_defer

    // If openDefer is true, the fields below record values about the stack
    // frame and associated function that has the open-coded defer(s). sp
    // above will be the sp for the frame, and pc will be address of the
    // deferreturn call in the function.
    fd   unsafe.Pointer // funcdata for the function associated with the frame
    varp uintptr        // value of varp for the stack frame
    // framepc is the current pc associated with the stack frame. Together,
    // with sp above (which is the sp associated with the stack frame),
    // framepc/sp can be used as pc/sp pair to continue a stack trace via
    // gentraceback().
    framepc uintptr
}

关于你的问题,大概解释一下:
首先,defer关键字后的函数调用执行会在函数返回前发生;
然后,defer关键字的执行顺序是倒序的,也就是写在代码最下方的defer先被调用。这是因为defer实际是一个链表(上面代码中的link *_defer,并且运行时会将后出现的defer追加到链表的最前面,而实际执行时又是从链表头开始执行,所以是倒序。

上面两点应该没什么问题,主要就是你这里a的赋值问题了。

defer关键字后面跟的是一个函数调用,你这里的其实本质上一样:都是调用了带参数的函数并把a作为参数传入,只不过是一个匿名函数。

注意了,defer关键字在代码运行到它(不是调用执行它)的时候,会直接拷贝函数参数,也就是当前函数参数的值,而又是值传递,不是地址传递。所以,在代码执行到这两句时,就已经把当前的a传进去了,因此输出的是1

的不同地方在哪里呢?它没有参数。我们知道,如果函数没有参数,当它内部调用一个变量时,就会去作用域外的作用域找,因此它用的是main()里的a。那么这个a为什么会输出2?就是前面说的:defer关键字后的函数调用会在函数返回前发生,而那时,defer的这个匿名函数作用域外的a已经是2了。

我们对照代码看一下,代码中,除了前面提到的link,还有一个fn *funcval,这货就是defer后面的函数。
当代码运行到defer的这一句时,会创建一个新的_defer结构体,并将defer后面的函数引用传递进来给fn,因此这个fn指向的还是原来那个匿名函数,则它们的作用域自然也相同。

我这里把你问题中的代码稍微改一下,根据上面我说的这些东西,猜猜输出是什么:

package main

import "fmt"

func main() {
    a := 1
    defer func() {
        fmt.Println("一:", a)
    }()

    defer fmt.Println("二:", a)


    defer func(a *int) {
        fmt.Println("四:", *a)
    }(&a)

    a ++
    defer func(a int) {
        fmt.Println("三:", a)
    }(a)
}

输出结果如下:

三: 2
四: 2
二: 1
一: 2

defer 执行的顺序,可以看成是栈,先进后出,后进先出,所以defer执行顺序是:三、二、一

对于变量,就是引用传递和拷贝变量的区别:闭包里边使用外边的变量的时候,应该获取的是变量地址对应的变量值,
那么defer“一”那个闭包里边变量a,此时已经变成了2
defer“二”,在a++之前已经传递了a变量的拷贝,就是a=1
defer“三”,同理,在a++之前已经传递了a变量的拷贝,就是a=1

因为 二 和 三 都是传的参数,在执行 defer 那一行的时候,参数已经定了;而 一 是直接使用的 main 函数里面 a

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏