Exploring Defers
Sources
When first learning Go, the immediate keyword that stands out to most developers is probably defer. It is probably one of the most unique things about Go and requires a small shift in thinking. On the surface it looks simple — run this when the function exits — but the details of when arguments are evaluated, how closures interact, and how pointers behave can produce surprising results. This post walks through those edge cases.
defer foo()
In the Defer, Panic, and Recover article the example provided is that one of opening and closing a file. Let’s take a closer look at some of the interactions that can play out using defers.
type SafeVal struct {
mu sync.Mutex
val int
}
func (sv *SafeVal) Set(i int) {
sv.mu.Lock()
defer sv.mu.Unlock()
sv.val = i
}
func (sv *SafeVal) Get() int {
sv.mu.Lock()
defer sv.mu.Unlock()
return sv.val
}
At work, usually am trying to aggregate data from multiple services and creating a clean copy in a centralized structure. So, I personally find this pattern super helpful when working with a critical section that needs to be extracted. And most of my interaction with defer is a simple function call. But let’s take that a little further and explore the interaction between defer, closures and pointers. The blog states that the “A deferred function’s arguments are evaluated when the defer statement is evaluated.”
func BasicDefer(i int) int {
defer fmt.Println("Inside Defer", i)
i++
return i
}
func main() {
k := 3
fmt.Println("Value Returned", BasicDefer(k))
}
Inside Defer 3
Value Returned 4
Sounds simple enough. But what if you need to print it twice. Well there are two ways immediate ways to do that. One is that you can just add another defer and print again. And remember defer follows LIFO (last in first out) so we will have something like this.
func BasicDeferTwo(i int) int {
defer fmt.Println("Inside Defer B", i)
defer fmt.Println("Inside Defer A", i)
i++
return i
}
Inside Defer A 3
Inside Defer B 3
Value Returned 4
Ok, that does what we need it to do. Now let’s try the second approach where we combine them into a singular defer.
func ComplexDefer(i int) int {
defer func() {
fmt.Println("Inside Defer A", i)
fmt.Println("Inside Defer B", i)
}()
i++
return i
}
Inside Defer A 4
Inside Defer B 4
Value Returned 4
Well, that’s slightly unexpected. We accidentally created a closure. This is super easy to do and many of us may not even realize that we did that. This can result in unexpected behaviour. Remember that the first rule of defer was that the arguments of a deferred function are evaluated at statement evaluation — so we can easily fix that bug by passing in the parameter explicitly.
Warning: An anonymous function used as a deferred call without explicit argument passing creates a closure that captures the surrounding variable by reference — not a snapshot of its value at the time
deferwas called. This is a very easy trap to fall into.
func ComplexDefer(i int) int {
defer func(i int) {
fmt.Println("Inside Defer A", i)
fmt.Println("Inside Defer B", i)
}(i)
i++
return i
}
Inside Defer A 3
Inside Defer B 3
Value Returned 4
Now that we have seen how closures interact with defer, a natural question is: what happens with pointers? Since a pointer is itself a value (an address), the same argument-evaluation rules apply — but when the dereference happens changes the result. Let’s revisit the basic example from the beginning of the article and swap the literal for a pointer.
func BasicDefer(i *int) int {
defer fmt.Println("Inside Defer", *i)
*i++
return *i
}
Inside Defer 3
Value Returned 4
Even when switching to the pointer the behaviour here is the same as our previous BasicDefer example. And this is because *i is being dereferenced and the value is being passed in as a copy into the Println. so if we were to change the example to use a closure-based defer instead:
func ComplexPointerDeferA(i *int) int {
defer func() {
fmt.Println("Inside Defer", *i)
}()
*i++
return *i
}
Inside Defer 4
Value Returned 4
Now we can easily see that this behaviour is expected. Since the dereference happens inside the function. And in this case it will not matter if the value pointer value is passed in since the initial value is a pointer and not a copy of the original value.
func ComplexPointerDeferB(i *int) int {
defer func(i *int) {
fmt.Println("Inside Defer", *i)
}(i)
*i++
return *i
}
Inside Defer 4
Value Returned 4
However as many of you may have guessed it. We can simply dereference the pointer at the moment of invocation and we will yield the original result.
func ComplexPointerDeferC(i *int) int {
defer func(i int) {
fmt.Println("Inside Defer", i)
}(*i)
*i++
return *i
}
Inside Defer 3
Value Returned 4
Hopefully this clarifies some of the weirdness you may have encountered when working with defers and anonymous functions.
Key takeaways:
- A deferred function’s arguments are evaluated at the time the
deferstatement is evaluated, not when the deferred function runs. - Deferred calls execute in LIFO order.
- An anonymous function used as a defer creates a closure — pass values explicitly as arguments if you need a snapshot at defer time.
- Pointer dereferences inside a deferred closure will see the mutated value; dereference at the defer statement to capture the original.