5 More Gotchas of Defer in Go — Part III

Gotchas and tricks about defer.

Inanc Gumus
Learn Go Programming
8 min readJan 18, 2018

--

Read the previous posts of this series here: Part I and Part II. This one is more about tricks rather than gotchas of deferring.

#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
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 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.

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

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
}
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 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
}
Catches and logs the error of f.Close()

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:

--

--