5 More Gotchas of Defer in Go — Part III
Gotchas and tricks about defer.


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

Example
func do() {
recover()
panic("error")
}
Output
It couldn’t catch the panic.
panic: error


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


#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 successful 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 deferring.
👉 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, the 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.



UPDATE: This problem looks like it doesn’t exist with http. I need to find a better example for that. So, it may still be valid for some code, but not for http. Check out this discussion: https://medium.com/@mafredri/great-article-thanks-c0e88d4df19e. Thanks, Mathias Fredriksson and Patrycja Szabłowska, for validating Mathias’s point. Check out it here: https://medium.com/@szablowska.patrycja/thanks-for-the-article-bdcca5eda295.

#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 lose 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
}


Better
It’s better to check the errors and do not just delegate and forget. You can simplify the code below 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
}


Better
You can also use 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. Wrapping an error with another one will add 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, the 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 funds, as I’ve explained here before (by using an 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 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.

Alright, that’s all for now. Thank you for reading so far.
Let’s stay in touch:
- 📩 Join my newsletter
- 🐦 Follow me on Twitter
- 📦 Get my Go repository for free tutorials, examples, and exercises
- 📺 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.