Original post

5 More Gotchas of Defer in  — Part II

Protect yourself from basic defer gotchas. For beginners.

We’re continuing our journey of defer gotchas.

Read Part I if you’ve missed it.

5 Gotchas of Defer in Go (Golang) — Part I

If you don’t know anything about defer please read this starter article first.

Go Defer Simplified with Practical Visuals

#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, I wanted to show you 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 which 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.

If you don’t know the difference between = 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 also 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.

It’s like calling fmt.Print() with “Hello”. Read this article to understand this behavior better.

#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 look to the same i which is 3 (its recent value after the loop ends).

All deferred funcs capture the same i, the loop ends (sets it to 3) and they see it as 3.

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 save 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)
}
I put the addresses of i variables inside the code, so, you can see what’s going on.

#5 — Point of no return?

Returning values from a deferred func practically have 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 defer.

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:


5 Gotchas of Defer in Go (Golang) — Part II was originally published in Learn Go Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.