Defer execution order

August 26, 2024

Let’s talk about some of the nuance surrounding defer that’s often lost, or simply forgotten.

Defer statements

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. That is, if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.

Let’s take this point by point.

When execution encounters a deferred function:

“function value and parameters are evaluated as usual” … “but the actual function is not invoked”

This bites all of us when we’re learning. And some of us it continues to bite for years into the future!

But what does it mean, exactly?

Let’s illustrate with an example.

func foo() {
	var i int
	defer fmt.Println(i)
	i = 10
	fmt.Println("foo")
}

What do you think this code will print when executed?

Try it if you like, to see that it prints:

foo
0

Does this surprise you?

The arguments to the deferred fmt.Println function are evaluated at the time the defer is encountered during execution, and at that point i == 0… but the function itself isn’t executed until the surrounding foo function returns, so after foo is printed.

Now, what do you think this will print?

func foo() {
	var i int
	defer func(p *int) { fmt.Println(*p) }(&i)
	i = 10
	fmt.Println("foo")
}

In this case, we’re passing a pointer to i to the deferred function. The argument is still evaluated immediately, as before… but in this case, when foo exits and the anonymous deferred function is executed, the value pointed to by i has changed, and we get the following output:

foo
10

Try it.

The function to be deferred is also evaluated immediately, not upon function exit. To demonstrate:

func main() {
	defer bar()()
	fmt.Println("Hello, 世界")
}

func bar() func() {
	fmt.Println("a")
	return func() {
		fmt.Println("b")
	}
}

This will output the following:

a
Hello, 世界
b

And the final point: If the function being deferred evaluates to nil, it will panic, but after the function returns, not at the point that defer is encountered:

func main() {
	defer bar()()
	fmt.Println("Hello, 世界")
}

func bar() func() {
	fmt.Println("a")
	return nil
}

This code will panic after some output:

a
Hello, 世界
panic: runtime error: invalid memory address or nil pointer dereference

See for yourself

Quotes from The Go Programming Language Specification Language version go1.23 (June 13, 2024)


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Context cancelation

As already mentioned, the context.Context type serves two purposes: It manages cancelation signals, and it allows passing request-scoped data. In my opinion, these two purposes are distinct enough that they probably should be handled by two separate mechanisms. But alas, that’s not what we have. However, I think it’s still useful to think of the two uses as distinct. Most often (at least in a well-designed application), the primary purpose of a context is the propegation of cancelation signals.


Order of recovery

Yesterday we saw how panic plays with deferred functions, in terms of order of execution. Today we’ll take things up one level, and throw recover in there… there’s more to recover than just execution order. We’ll get to that next. Handling panics … The recover function allows a program to manage behavior of a panicking goroutine. Suppose a function G defers a function D that calls recover and a panic occurs in a function on the same goroutine in which G is executing.


Panicking and deferred functions

Handling panics … While executing a function F, an explicit call to panic or a run-time panic terminates the execution of F. Any functions deferred by F are then executed as usual. Next, any deferred functions run by F’s caller are run, and so on up to any deferred by the top-level function in the executing goroutine. At that point, the program is terminated and the error condition is reported, including the value of the argument to panic.

Get daily content like this in your inbox!

Subscribe