5 More Gotchas of Defer in Go — Part II
Protect yourself from basic defer gotchas.

#1 — Z to A
This can become a gotcha when you’re learning Go for the first time.

Example
func main() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
Output
Go runtime saves deferred funcs in a stack which means that they work in reverse order. Read this article to understand it better.
3
2
1
0



#2—Scope ate my param
This is actually a scope gotcha. However, how it behaves with defer and named result values.

Example
Let’s define a func which registers a deferred func to release the resource r
in defer after the func returns.

Create a reader that returns an error on Close
type reader struct{}func (r reader) Close() error {
return errors.New("Close Error")
}
Let’s try
reader
always returns an error when its Close()
method is called and release()
will call it inside defer, as seen in the image above.
r := reader{}err := release(r)fmt.Print(err)
Output
nil
err
is nil
but we were expecting: “Close Error”.
Why is that?
Assignment inside the deferred func’s if block shadows the named result value err
with a new err
variable. So, release()
returns the original err
result-value.

Solution
We need to use the original err
variable ofrelease()
. To do that, look at line #3 below. This code doesn’t declare a new err
variable. Instead, it uses the original err
result value (with =
instead of :=
). This solves the shadowing problem.

=
and :=
check out this article.


#3—Quick to run the params
Params passed to a deferred func are evaluated when the defer is registered, not when it runs. Check out this example in my other article.

Example
type message struct {
content string
}func (p *message) set(c string) {
p.content = c
}func (p *message) print() string {
return p.content
}
Let’s try it
func() {
m := &message{content: "Hello"} defer fmt.Print(m.print()) m.set("World") // deferred func runs
}
Output
"Hello"

Why the output is not “World”?
Here in defer, fmt.Print
is deferred until the func returns but m.print()
is evaluated immediately. So, m.print()
will return “Hello,” and it’s saved aside until the surrounding func returns.


#4—Captured in a Loop
A deferred func will see the most recent values of the surrounding variables when it runs — except the passed params to it. Let’s see how it happens inside a loop.

Example
Let’s defer a closure inside a loop.
for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i)
}()
}
Output
3
3
3
Why?
When the code runs, the deferred func sees the recent value of i
. That’s because, when the defers are registered, Go runtime captures the address of i.
After the for loop ends i
becomes 3. So, when the defers run, all of them will refer to the same i
, which is 3 (its recent value after the loop ends).


Solution
Pass parameters directly to defer.
for i := 0; i < 3; i++ {
defer func(i int) {
fmt.Println(i)
}(i)
}
Output
2
1
0
Why it works?
Because, here, Go runtime creates different i
variables and saves them aside with the correct values. Now, defers don’t see the original i
variable of the loop anymore. They see their own i
local variables.
Solution #2
Here, we’re intentionally shadowing i
with a new i
inside the scope of the for loop. I‘m not a fan of this style. So, I’ve added this here after a discussion on Hacker News.
for i := 0; i < 3; i++ {
i := i
defer func() {
fmt.Println(i)
}()
}
Solution #3
If there’s only one func call, you can use it directly.
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}



#5 — Point of no return?
Returning values from a deferred func practically has no effect on the caller. However, you can still use named result values to change the result values.

Example
func release() error {
defer func() error {
return errors.New("error")
}() return nil
}
Output
nil

Solution
Defer can change the named result values.
func release() (err error) {
defer func() {
err = errors.New("error")
}() return nil
}
Output
"error"
Why does it work?
Here, we’re assigning a new value to release func’s err result value inside the defer. And, the func returns that value. The defer doesn’t directly return value here. It helps to return the value.

Tip
You don’t need to use defer everywhere. You can also just return the error directly without deferring.
Defers are handier when there are multiple return paths, and you want to catch them all in a single place. Better to think more about how you can simplify your code further.



Alright, that’s all for now. Thank you for reading so far.
Let’s stay in touch:
- 📩 Join my newsletter
- 🐦 Follow me on Twitter
- 📺 Learn Go with my Go Bootcamp Course
- ❤️ Do you want to help? Please clap and share the article. Let other people also learn from this article.