【go编程之旅】How Does defer Statement Work?

ℹ️ This article is based on Go 1.12.

defer statement is a convenient way to execute a piece of code before a function returns, as explained in Golang specification:

Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred.

Here is an example of the LIFO (last-in-first-out) implementation:

func main() {
defer func() {
println(defer 1)
defer func() {
println(defer 2)
}defer 2 <- Last in, first to go out
defer 1

Let’s have a look at the internals and then a more complex case.

Internals implementation

Go runtime implements the LIFO with a linked list. Indeed, a defer struct has a link to the next one to be executed:

type _defer struct {
siz int32
started bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer // next deferred function to be executed

When a new defer method is created, it is attached to the current Goroutine and the previous one is linked to the new one as the next function to be executed:

func newdefer(siz int32) *_defer {
var d *_defer
gp := getg() // get the current goroutine
// deferred list is now attached to the new _defer struct
d.link = gp._defer
gp._defer = d // the new struct is now the first to be called
return d

The successive calls will now unstack the deferred functions from the top:

func deferreturn(arg0 uintptr) {
gp := getg() // get the current goroutine
d:= gp._defer // copy the deferred function to a variable
if d == nil { // if there is not deferred func, just return
fn := d.fn // get the function to call
d.fn = nil // reset the function
gp._defer = d.link // attach the next one to the goroutine
freedefer(d) // freeing the _defer struc
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) // call the func

As we can see, we do not loop of the deferred functions, it is unstacked one by one. This behavior is confirmed by the generated ASM code:

// first deferred func
0x001d 00029 (main.go:6) MOVL $0, (SP)
0x0024 00036 (main.go:6) PCDATA $2, $1
0x0024 00036 (main.go:6) LEAQ "".main.func1·f(SB), AX
0x002b 00043 (main.go:6) PCDATA $2, $0
0x002b 00043 (main.go:6) MOVQ AX, 8(SP)
0x0030 00048 (main.go:6) CALL runtime.deferproc(SB)
0x0035 00053 (main.go:6) TESTL AX, AX
0x0037 00055 (main.go:6) JNE 117// second deferred func
0x0039 00057 (main.go:10) MOVL $0, (SP)
0x0040 00064 (main.go:10) PCDATA $2, $1
0x0040 00064 (main.go:10) LEAQ "".main.func2·f(SB), AX
0x0047 00071 (main.go:10) PCDATA $2, $0
0x0047 00071 (main.go:10) MOVQ AX, 8(SP)
0x004c 00076 (main.go:10) CALL runtime.deferproc(SB)
0x0051 00081 (main.go:10) TESTL AX, AX
0x0053 00083 (main.go:10) JNE 101// end of main func
0x0055 00085 (main.go:18) XCHGL AX, AX
0x0056 00086 (main.go:18) CALL runtime.deferreturn(SB)
0x005b 00091 (main.go:18) MOVQ 16(SP), BP
0x0060 00096 (main.go:18) ADDQ $24, SP
0x0064 00100 (main.go:18) RET
0x0065 00101 (main.go:10) XCHGL AX, AX
0x0066 00102 (main.go:10) CALL runtime.deferreturn(SB)
0x006b 00107 (main.go:10) MOVQ 16(SP), BP
0x0070 00112 (main.go:10) ADDQ $24, SP
0x0074 00116 (main.go:10) RET

The method deferproc is called twice and internally calls the method newdefer we have seen previously to register our function as deferred methods. Then, at the end of the function, the deferred method will be called one by one thanks to the deferreturn function.

The Go library did show us that the struct _defer is also linked to a _panic *_panic attribute. Let’s see where it could be useful through another example.

Defer and return value

The only way for a deferred function to access the returned result is to use a named result parameter as explained in the specification:

If the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned

Here is an example:

func main() {
fmt.Printf("with named param, x: %d\n", namedParam())
fmt.Printf("without named param, x: %d\n", notNamedParam())
}func namedParam() (x int) {
x = 1
defer func() { x = 2 }()
return x

func notNamedParam() (int) {
x := 1
defer func() { x = 2 }()
return x
}with named param, x: 2
without named param, x: 1

Once this behavior is acknowledged, we could mix it with the recover function. Indeed, as explained in the “defer, panic and recover” blog post:

Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions.

As we have seen, the _defer struct is linked to a _panic attribute that is linked during a panic call:

func gopanic(e interface{}) {
var p _panic
d := gp._defer // current attached defer on the goroutine
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

Indeed, the method gopanic is called in case of panic before calling the deferred functions:

0x0067 00103 (main.go:21)   CALL   runtime.gopanic(SB)
0x006c 00108 (main.go:21) UNDEF
0x006e 00110 (main.go:16) XCHGL AX, AX
0x006f 00111 (main.go:16) CALL runtime.deferreturn(SB)

Here is an example of the recover function takes advantage of the named result parameter:

func main() {
fmt.Printf("error from err1: %v\n", err1())
fmt.Printf("error from err2: %v\n", err2())

func err1() error {
var err error

defer func() {
if r := recover(); r != nil {
err = errors.New("recovered")

return err

func err2() (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("recovered")

return err
}error from err1: <nil>
error from err2: recovered

The conjunction of both allow us to work properly with the recover function and our error we would like to return to the caller.
To conclude this article about the deferred function, let’s have a look at the improvements to it.

Performance improvement

The last version that have improved the usage of defer is the Go version 1.8. We can see that with running the benchmark in the Go library (between 1.7 and 1.8):

name         old time/op  new time/op  delta
Defer-4 99.0ns ± 9% 52.4ns ± 5% -47.04% (p=0.000 n=9+10)
Defer10-4 90.6ns ±13% 45.0ns ± 3% -50.37% (p=0.000 n=10+10)

This improvement has been done thanks to this CL that improve the way it is allocated with avoiding stack growth.

There is also an optimization for the defer statement with no arguments that skip a memory copy. Here is a benchmark for a deferred function with/without argument:

name     old time/op  new time/op  delta
Defer-4 51.3ns ± 3% 45.8ns ± 1% -10.72% (p=0.000 n=10+10)

It is also now 10% faster thanks to the second optimization.


未经允许随便转载:看过够 » 【go编程之旅】How Does defer Statement Work?

赞 (2) 打赏

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址