Go Secret - Defer: What you know about DEFER in Go is not enough!
This article looks at some not-so-obvious things about the "defer" statement in Go, showing you a few things you might not know.
Hey, you think you know everything there is about ‘defer’? Well, think twice.
Believe me, it’s not just about postponing a function’s execution until your current function wraps up. Whether you’re new to Go or an old hand, this guide has something valuable for you.
So, let’s get started.
1. Evaluating Arguments
Have you ever realized that the arguments for a deferred function in Go are actually set when you first hit the defer statement, not when the deferred function finally runs?
Check out this example for clarity:
package main
import "fmt"
func main() {
i := 0
defer fmt.Println(i)
i++
}
So, what do you think will pop up in the console?
Even though fmt.Println(i)
only fires off at the end, after i
is incremented to 1, it still outputs 0. Weird, isn't it?
That’s because i
is evaluated at the moment the defer statement runs, and at that point, i
is zero.
"Because the
defer
orfmt.Println(i)
uses a copy ofi
frommain
, right?"
No.
Such an explanation can lead to misunderstandings, making one think that defer
keeps and later uses a separate copy of i.
Here's a clearer example:
package main
import "fmt"
func main() {
i := 0
defer fmt.Println(i + 1)
fmt.Println(i + 1)
i++
}
In this case, defer
does not create a copy of i
and then compute i + 1
.
The variable i itself isn't being copied, instead, the value of i is read and used in the arithmetic operation i+1, defer
holds the result of the argument (i+1), not a copy of the variable i.
The key point is, “the deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns”.
“So how do I work around this?”
Fixing this issue is actually quite easy, you simply move the argument into the deferred function’s body:
func main() {
i := 0
defer func() {
fmt.Println(i)
}()
i++
}
// 1
“Already knew this, what’s next?”
If that didn’t catch you off guard, stay tuned, the next section dives deeper but with a twist in another Go feature.
2. Evaluating Function Receivers
You’ve got the hang of how deferred function arguments work, right? So, how about diving into function receivers and defer?
Check this sample:
package main
import "fmt"
type Person struct {
Name string
}
func (p Person) SayHi() {
fmt.Println("Hi, my name is", p.Name)
}
func main() {
writer := Person{Name: "Joe"}
defer writer.SayHi()
// fix the wrong name
writer.Name = "Aiden"
}
Now, this one’s different. See, writer.SayHi()
has zero parameters, unlike our previous case.
So, what’s gonna show up on your screen this time?
Even though writer.SayHi()
comes in at the end, you'd expect it to say, "Hi, my name is Aiden" right?
It's actually gonna say, "Hi, my name is Joe".
In our case, when the defer statement defer writer.SayHi()
is executed, the writer
value at that particular point in time is taken or "evaluated" for the SayHi
method.
So, even though we update writer.Name
to Aiden
later in the code, the deferred call has already evaluated and captured the earlier value of writer
, which is Joe
.
This can be dangerous when you want to do something with the object after you initialize or change it.
“Hey, will the solution from section one work here?”
Absolutely, it will.
But hang on, there’s another way to get around this. You could swap out the value receiver for a pointer receiver, like this:
// Use *Person, not Person
func (p *Person) SayHi() {
fmt.Println("Hi, my name is", p.Name)
}
Just so we’re clear, this isn’t the ideal solution since using a pointer here is a bit extra since we’re not really changing the ‘person’ object. Consider this more of a learning point.
3. Don’t worry about a panic, Defer’s got your back
When you use the ‘defer’ keyword, the corresponding function executes after the main function ends, even if there’s a panic along the way. This is particularly useful for doing some essential cleanup like shutting down files or releasing locks.
“So, can this help keep the service running?”
You bet.
It’s a common practice to pair defer
with recover
for this very reason, to manage unexpected panics and prevent service interruptions:
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
panic("This is a panic!")
}
// Recovered from panic: This is a panic!
Web server libraries, such as gin and iris, are way ahead of you, they’ve got built-in middleware called Recover
that leverages this very approach.
4. Defer can change the outcome of function
Suppose you’ve got named return values, like ‘x’ in the example that follows:
func deferMe() (x int) {
x = 1
return
}
With a clever use of defer
, you can actually change what the function returns before it’s done.
func main() {
fmt.Println(deferMe()) // 2
}
func deferMe() (x int) {
defer func() { x *= 2 }()
x = 1
return
}
But hold on, while this is a neat trick, be cautious.
Doing this can make your code tough to follow and even tougher to debug. So maybe think twice before you put this into your production code.
5. Defer order — Last In First Out
Remember, the execution of defer statements follows a last-in, first-out (LIFO) pattern. If you’ve lined up several defer statements, they’ll execute in reverse order to how you set them up.
Take a look at this example:
func main() {
defer fmt.Println("Defer 1")
defer fmt.Println("Defer 2")
defer fmt.Println("Defer 3")
}
// Defer 3
// Defer 2
// Defer 1
Defer can do a lot of tricks, but that doesn’t mean you should use it for showing your skill.