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

5 More Gotchas of Defer in Go — Part III

Gotchas and tricks about defer.

This article is more about tricks rather than gotchas of defer.

Read the following posts from this series to discover more gotchas and tricks about defer:

  • Part I
  • Part II
  • Part III (this)
  • Part IV (incoming — the last one)

If you don’t know how defer works please read this starter post first:

Go Defer Simplified with Practical Visuals

#1 — Calling recover outside of a deferred func

You should call recover() always inside a deferred func. When a panic occurs, calling recover() outside of defer will not catch it and the recover() will return nil.

Example

func do() {
recover()
panic("error")
}

Output

It couldn’t catch the panic.

panic: error
Will panic

Solution

Just by using recover() inside defer you can prevent this problem.

func do() {
defer func() {
r := recover()
fmt.Println("recovered:", r)
}()
  panic("error")
}

Output

recovered: error
Will recover from the error

#2— Calling defer in the wrong order

This gotcha is from 50 shades of Go, here.

Example

This code will panic when http.Get fails.

func do() error {
res, err := http.Get("http://notexists")
defer res.Body.Close()
if err != nil {
return err
}
  // ..code...
  return nil
}

Output

panic: runtime error: invalid memory address or nil pointer dereference

Why?

Because, here, we didn’t check whether the request was successful or not. Here it fails, and we call Body on a nil variable (res), hence the panic.

Solution

Always use defer after a succesful resource allocation. For this example this means: Use defer only if http.Get is succesful.

func do() error {
res, err := http.Get("http://notexists")
if res != nil {
defer res.Body.Close()
}
  if err != nil {
return err
}
  // ..code...
  return nil
}

With the above code, when there’s an error, the code will return the error, otherwise, it’ll close res.Body when the func returns in defer.

👉 Side-Note

Here, you also need to check whether resp is nil, this is a caveat for http.Get. Usually, when there is an error, the response will be nil and an error will be returned. But, when you get a redirection error, response will not be nil but there’ll be an error. With the above code, you’re ensuring that you’re closing the response body. You also need to discard the data received if you’re not going to use it. More details here.

Do not forget to uncomment the code line that I’ve marked in the code

#3— Not checking for errors

Just delegating the clean-up logic to defer doesn’t mean that the resource will be released without a problem. You‘ll also miss probably useful error messages and loose your ability to diagnose hidden problems by sinking them.

Not good

Here, f.Close() may return an error but we wouldn’t be aware of it.

func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
defer f.Close()
  // ..code...
  return nil
}
Cannot catch the error of f.Close()

Better

It’s better to check the errors and do not just delegate and forget. You can simplify the below code by taking the code inside the defer to a helper func, here it’s kind of messy just to show you the problem.

func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
  defer func() {
if err := f.Close(); err != nil {
// log etc
}
}()
  // ..code...
  return nil
}
Catches and logs the error of f.Close()

Better

You can also use a named result values to return back the error inside defer.

func do() (err error) {
f, err := os.Open("book.txt")
if err != nil {
return err
}
  defer func() {
if ferr := f.Close(); ferr != nil {
err = ferr
}
}()
  // ..code...
  return nil
}

👉 Side-Note

You can also use this package to wrap multiple errors. This may be necessary, because, f.Close inside defer may swallow any errors before it. By wrapping an error with another one will write this information to your log, so you can diagnose problems with more data.

👉 You can also use this package to catch the places that you’re not checking for errors.

#4— Releasing the same resource

The section third above has one caveat: If you try to close another resource using the same variable, it may not behave as expected.

Example

This innocent looking code tries to close the same resource twice. Here, the second r variable will be closed twice. Because, r variable will be changed for the second resource below.

func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
  defer func() {
if err := f.Close(); err != nil {
// log etc
}
}()
  // ..code...
  f, err = os.Open("another-book.txt")
if err != nil {
return err
}
  defer func() {
if err := f.Close(); err != nil {
// log etc
}
}()
  return nil
}

Output

closing resource #another-book.txt
closing resource #another-book.txt

Why

As we’ve seen before, when defers run, only the last variable gets used. So, f variable will become the last one (another-book.txt). And, both defers will see it as the last one.

Solution

func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
  defer func(f io.Closer) {
if err := f.Close(); err != nil {
// log etc
}
}(f)
  // ..code...
  f, err = os.Open("another-book.txt")
if err != nil {
return err
}
  defer func(f io.Closer) {
if err := f.Close(); err != nil {
// log etc
}
}(f)
  return nil
}

Output

closing resource #another-book.txt
closing resource #book.txt

👉 You can also easily avoid this by using funcs as I’ve explained in here before (by using opener/closer pattern).

#5—panic/recover can get and return any type

You may think that you always need to put string or error into panic.

With string:

func errorly() {
defer func() {
fmt.Println(recover())
}()
  if badHappened {
panic("error run run")
}
}

Output

"error run run"

With error:

func errorly() {
defer func() {
fmt.Println(recover())
}()
  if badHappened {
panic(errors.New("error run run")
}
}

Output

"error run run"

Accepts any type

As you see panic can accept a string and as well as an error type. This means that you can put “any type” into panic and get that value back from recover inside defer. Check this out:

type myerror struct {}
func (myerror) String() string {
return "myerror there!"
}
func errorly() {
defer func() {
fmt.Println(recover())
}()
  if badHappened {
panic(myerror{})
}
}

Why

That’s because, panic accepts an interface{} type which practically means: “any type” in Go.

This is how panic is declared in Go:

func panic(v interface{})

Its friend recover is declared like this:

func recover() interface{}

So, basically, it works like this:

panic(value) -> recover() -> value

recover just returns the value passed to panic.

👍 Allright peeps, that’s it for now. See you in the next post. Until then you may follow me on twitter and get the latest tips and tricks.

🔥 I’m also creating an online course for Go: Join to my newsletter

“ Let’s stay in touch weekly for new tutorials and tips “

I tweet puzzles, tips and tricks almost daily, follow me on twitter to get them if you like.

Example:

Don’t stop now, learn more:

More?

💓 Share this post with your friends. Thank you!!! 💓

I’m creating an online course for Go → Join to my newsletter

Let’s stay in touch ~weekly for new tutorials and tips.

👍 Subscribers will get the discounted price (and even free) for my course when it’s get published.


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

Please follow and like us:
error0