SoFunction
Updated on 2025-03-03

Go programmers have stepped on error handling of defer pit errors

Preface

First statement: I have been cheated.

When I was writing a Go column before, I wrote an article:Go column | Error handling: defer, panic and recover. A friend left a message saying: I understand the truth, but I still don’t know how to use it, and there are always inexplicable problems.

It's right if something goes wrong. This little thing is very bad and makes mistakes if you are not careful.

So, in the face of this situation, we are unreasonable today. I just took the code I had collected for many years, and with my years of experience in stumbled on pits and writing bugs, I would stand and take this pit over.

1. Simple examples

Let’s first give a simple example to warm up:

package main
import (
    "fmt"
)
func main() {
    defer func() {
        ("first")
    }()
    defer func() {
        ("second")
    }()
    ("done")
}

Output:

done
second
first

This is relatively simple. The execution order of the defer statement is executed in reverse order of the call defer statement.

2. Do not use defer statements in the for loop

See what's wrong with this code?

for _, filename := range filenames {
    f, err := (filename)
    if err != nil {
        return err
    }
    defer ()
}

This code is actually very dangerous and will likely use up all file descriptors. Because the defer statement will not be executed until the last moment of the function, which means that the file cannot be closed at all. So remember not to use the defer statement in the for loop.

So how to optimize it? You can write a function separately to the loop body, so that the closing function will be called every time the loop is looped.

as follows:

for _, filename := range filenames {
    if err := doFile(filename); err != nil {
        return err
    }
}
func doFile(filename string) error {
    f, err := (filename)
    if err != nil {
        return err
    }
    defer ()
}

3. Do not use naming to return values ​​when defining functions

Let’s see what the output results of these three functions are?

package main
import (
    "fmt"
)
func a() (r int) {
    defer func() {
        r++
    }()
    return 0
}
func b() (r int) {
    t := 5
    defer func() {
        t = t + 5
    }()
    return t
}
func c() (r int) {
    defer func(r int) {
        r = r + 5
    }(r)
    return 1
}
func main() {
    ("a = ", a())
    ("b = ", b())
    ("c = ", c())
}

Announced answer:

a =  1
b =  5
c =  1

Have you answered correctly?

To be honest, when I first saw this result, I was quite puzzled and had no idea what was going on.

But we can see that these three functions have a common feature, they all have a named return value, and they all refer to this return value in the function.

There are two ways to quote: closure and function parameters.

Let’s look at the a() function first:

The closure modified the external variable through r++, and the return value became 1.

Equivalent to:

func aa() (r int) {
    r = 0
    // Before return, execute the defer function    func() {
        r++
    }()
    return
}

Let’s look at the b() function again:

The local variable t is modified in the closure, while the external variable t is not affected, so 5 is still returned.

Equivalent to:

func bb() (r int) {
    t := 5
    // Assignment    r = t
    // Before return, execute the defer function    // The defer function did not modify the return value r, but only modified the variable t    func() {
        t = t + 5
    }()
    return
}

Finally, the c function:

Parameter passing is a value copy, and the actual parameter is not affected, so it still returns 1.

Equivalent to:

func cc() (r int) {
    // Assignment    r = 1
    // The r modified here is the value of the function parameter    // Copy the value and do not affect the actual parameter value    func(r int) {
        r = r + 5
    }(r)
    return
}

So, in order to avoid writing such unexpected code, it is best not to use named return values ​​when defining functions. Or if used, do not quote it in defer.

Let’s look at the following two examples:

func d() int {
    r := 0
    defer func() {
        r++
    }()
    return r
}
func e() int {
    r := 0
    defer func(i int) {
        i++
    }(r)
    return 0
}

d =  0
e =  0

The return value meets expectations, and you no longer have to rack your brains to guess.

4. If the function of the defer expression is behind panic, this function cannot be executed.

func main() {
    panic("a")
    defer func() {
        ("b")
    }()
}

The output is as follows, b is not printed.

panic: a
goroutine 1 [running]:
()
    :87 +0x4ce
exit status 2

And if defer is in front, it can be executed.

func main() {
    defer func() {
        ("b")
    }()
    panic("a")
}

Output:

b
panic: a
goroutine 1 [running]:
()
    :90 +0x4e7
exit status 2

5. Execution order

Check out the execution order of the following code:

func G() {
    defer func() {
        ("c")
    }()
    F()
    ("Continue to execute")
}
func F() {
    defer func() {
        if err := recover(); err != nil {
            ("Catch exception:", err)
        }
        ("b")
    }()
    panic("a")
}
func main() {
    G()
}

The order is as follows:

  • Call the G() function;
  • Call the F() function;
  • When you encounter panic in F(), terminate immediately and do not execute the code after panic;
  • Execute the defer function in F(), encounter the recover catch error, continue to execute the defer code, and then return;
  • Execute the subsequent code of the G() function, and finally execute the defer function in G().

Output:

Catch exception: a
b
Continue to execute
c

5. Capture exception execution order

Check out the execution order of the following code:

func G() {
    defer func() {
        if err := recover(); err != nil {
            ("Catch exception:", err)
        }
        ("c")
    }()
    F()
    ("Continue to execute")
}
func F() {
    defer func() {
        ("b")
    }()
    panic("a")
}
func main() {
    G()
}

The order is as follows:

  • Call the G() function;
  • Call the F() function;
  • When you encounter panic in F(), terminate immediately and do not execute the code after panic;
  • Execute the defer function in F(). Since there is no recovery, panic will be thrown into G();
  • G() When receiving panic, it will not execute subsequent code, and directly execute the defer function;
  • The exception a thrown by F() is captured in defer, and then continue to execute and finally exit.

Output:

b
Catch exception: a
c

6. Function execution order

Check out the execution order of the following code:

func G() {
    defer func() {
        ("c")
    }()
    F()
    ("Continue to execute")
}
func F() {
    defer func() {
        ("b")
    }()
    panic("a")
}
func main() {
    G()
}

The order is as follows:

  • Call the G() function;
  • Call the F() function;
  • When you encounter panic in F(), terminate immediately and do not execute the code after panic;
  • Execute the defer function in F(). Since there is no recovery, panic will be thrown into G();
  • G() When receiving panic, it will not execute subsequent code, and directly execute the defer function;
  • Since there is no recovery, the exception a thrown by F() is directly thrown, and then exit.

Output:

b
c
panic: a
goroutine 1 [running]:
()
    :90 +0x5b
()
    :82 +0x48
()
    :107 +0x4a5
exit status 2

7. Execution order of exception capture by external functions

Check out the execution order of the following code:

func G() {
    defer func() {
        // goroutine outside recover        if err := recover(); err != nil {
            ("Catch exception:", err)
        }
        ("c")
    }()
    // Create goroutine Call the F function    go F()
    ()
}
func F() {
    defer func() {
        ("b")
    }()
    // goroutine internally throws panic    panic("a")
}
func main() {
    G()
}

The order is as follows:

  • Call the G() function;
  • Call the F() function through goroutine;
  • When you encounter panic in F(), terminate immediately and do not execute the code after panic;
  • Execute the defer function in F(). Since there is no recovery, panic will be thrown into G();
  • Since there is no recovery within goroutine, the goroutine external function, that is, the G() function, cannot be captured, and the program crashes and exits directly.

Output:

b
panic: a
goroutine 6 [running]:
()
    :96 +0x5b
created by 
    :87 +0x57
exit status 2

8. Recover’s return value problem

defer func() {
    if err := recover(); err != nil {
        ("Catch exception:", ())
    }
}()
panic("a")

Recover returns the interface {} type, not the error type, so if you use it like this, you will get an error:

 undefined (type interface {} is interface with no methods)

You can convert it like this:

defer func() {
    if err := recover(); err != nil {
        ("Catch exception:", ("%v", err).Error())
    }
}()
panic("a")

Or print the result directly:

defer func() {
    if err := recover(); err != nil {
        ("Catch exception:", err)
    }
}()
panic("a")

Output:

Catch exception: a

The above is all about this article. In fact, students who have written other languages ​​know that closing the file handle, releasing the lock and other operations are easy to forget. The Go language solves this problem well through defer, but you still have to be careful during use.

Source code address:/yongxinz/gopher/tree/main/sc

The above is the detailed content of the error handling of defer pits that Go programmers have stepped on. For more information about Go defer error handling, please pay attention to my other related articles!