Sitemap

Learn Go Programming

Visual, concise and detailed tutorials, tips and tricks about Go (aka Golang).

5 More Gotchas of Defer in Go — Part III

Gotchas and tricks about defer.

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.

Press enter or click to view image in full size

Example

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

Output

It couldn’t catch the panic.

panic: error
Will panic
Press enter or click to view image in full size

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.

Press enter or click to view image in full size

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
Press enter or click to view image in full size

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.

Press enter or click to view image in full size

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.

Press enter or click to view image in full size
Do not forget to uncomment the code line that I’ve marked in the code
Press enter or click to view image in full size

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,

and , 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.

Press enter or click to view image in full size

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()
Press enter or click to view image in full size

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()
Press enter or click to view image in full size

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
}
Press enter or click to view image in full size

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

Press enter or click to view image in full size

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
Press enter or click to view image in full size

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.

Press enter or click to view image in full size

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
Press enter or click to view image in full size

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

Press enter or click to view image in full size

With string:

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

Output

"error run run"
Press enter or click to view image in full size

With error:

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

Output

"error run run"
Press enter or click to view image in full size

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{})
}
}
Press enter or click to view image in full size

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:

--

--

Learn Go Programming
Learn Go Programming

Published in Learn Go Programming

Visual, concise and detailed tutorials, tips and tricks about Go (aka Golang).

Inanc Gumus
Inanc Gumus

Written by Inanc Gumus

Coder. Gopher. Maker. Stoic. Since 1992.

Responses (4)