defer #
defer
语句经常用于成对的操作,比如 打开文件/关闭文件
连接网络/断开网络
, 合理地使用 defer
不仅可以提高代码可读性,也降低了忘记释放资源造成的泄漏等问题。
正确使用 defer
语句的地方是在成功获取资之后。
断开网络连接 #
package main
import (
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://www.baidu.com")
// 此时资源有可能获取失败,执行 Close 导致 panic
// resp.Body.Close()
if err != nil {
panic(err)
}
defer func() {
err = resp.Body.Close() // 关闭资源
if err != nil {
log.Fatal(err)
}
}()
}
关闭文件句柄 #
package main
import (
"os"
)
func main() {
name := "/etc/hosts"
file, err := os.Open(name)
// 此时资源有可能获取失败,执行 Close 导致 panic
// file.Close()
if err != nil {
panic(err)
}
defer func() {
err = file.Close() // 关闭文件句柄
if err != nil {
panic(err)
}
}()
hosts := make([]byte, 1024)
_, err = file.Read(hosts)
if err != nil {
panic(err)
}
}
计算程序耗时 #
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
// 错误的写法,等于注册 defer 函数的时候就已经计算好了输出值
// defer fmt.Printf("executed time (%s)\n", time.Since(start))
// 正确的写法
defer func() {
fmt.Printf("executed time (%s)\n", time.Since(start))
}()
time.Sleep(3 * time.Second) // 模拟程序耗时
}
// $ go run main.go
// 输出如下
/**
executed time (3.0021534s)
*/
defer 不会执行的情况 #
os.Exit
会直接退出程序,不用调用已经注册的 defer
函数。
package main
import "os"
func main() {
println("hello world")
defer func() {
println("hello defer")
}()
os.Exit(0)
}
// $ go run main.go
// 输出如下
/**
hello world
*/
通过输出结果可以看到,defer
并未执行,字符串 hello defer
没有输出,原因在于: 调用 os.Exit()
函数之后,程序会立即终止,
所有后面的代码和 defer
函数都不会执行。
recover #
recover
函数调用有着严格的要求,必须在 defer
函数中直接调用 recover
,否则 panic
将无法被捕获。
如果 defer
函数中调用的是经过包装的 recover
函数,panic
将同样无法被捕获。
recover 必须在 defer 中调用 #
错误的做法 #
package main
import "fmt"
func main() {
if r := recover(); r != nil { // 无法捕获到 panic
fmt.Printf("panic = %v\n", r)
}
panic("some error")
}
// $ go run main.go
// 输出如下
/**
panic: some error
...
...
exit status 2
*/
正确的做法 #
将 recover
函数放置在 defer
函数中调用。
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil { // 可以捕获到 panic
fmt.Printf("panic = %v\n", r)
}
}()
panic("some error")
}
// $ go run main.go
// 输出如下
/**
panic = some error
*/
recover 必须在 defer 中直接调用 #
错误的做法 #
package main
import "fmt"
func myRecover() {
if r := recover(); r != nil { // 无法捕获到 panic
fmt.Printf("panic = %v\n", r)
}
}
func main() {
defer func() {
myRecover()
}()
panic("some error")
}
// $ go run main.go
// 输出如下
/**
panic: some error
...
...
exit status 2
*/
错误的原因在于: defer
以匿名函数的方式运行,本身就等于包装了一层函数,
内部的 myRecover
函数包装了 recover
函数,等于又加了一层包装,变成了两
层包装,这时最外层的 panic
就无法被捕获了。
正确的做法 - 1 #
defer
直接调用 myRecover
函数,这样减去了一层包装,panic
就可以被捕获了。
package main
import "fmt"
func myRecover() {
if r := recover(); r != nil { // 无法捕获到 panic
fmt.Printf("panic = %v\n", r)
}
}
func main() {
defer myRecover()
panic("some error")
}
// $ go run main.go
// 输出如下
/**
panic = some error
*/
正确的做法 - 2 #
将 recover
函数放置在 defer
函数中调用,panic
就可以被捕获了。
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil { // 可以捕获到 panic
fmt.Printf("panic = %v\n", r)
}
}()
panic("some error")
}
// $ go run main.go
// 输出如下
/**
panic = some error
*/
多个 panic 只有一个被捕获 #
错误的做法 #
package main
import "fmt"
func foo() {
defer func() {
println("recover 1")
if err := recover(); err != nil { // 无法捕获到 panic
fmt.Printf("[1] recovered %d\n", err)
}
}()
defer func() {
println("recover 2")
if err := recover(); err != nil { // 无法捕获到 panic
fmt.Printf("[2] recovered %d\n", err)
}
}()
defer func() {
println("recover 3")
if err := recover(); err != nil { // 可以捕获到 panic
fmt.Printf("[3] recovered %d\n", err)
}
}()
defer func() {
println("panic 1")
panic(1)
}()
defer func() {
println("panic 2")
panic(2)
}()
defer func() {
println("panic 3")
panic(3)
}()
}
func main() {
foo()
}
// $ go run main.go
// 输出如下
/**
panic 3
panic 2
panic 1
recover 3
[3] recovered 1
recover 2
recover 1
*/
通过输出结果可以看到,即使抛出了多个 panic
, 也只有最后一个被捕获。
因为第一个 recover
函数执行完后,会影响到后面的 recover
函数 (第一个 recover
捕获错误后,后面的 recover
不会捕获到任何错误)。
正确的做法 #
如果希望抛出的多个 panic
全部被捕获,应该在 recover
函数执行完后再依次执行 panic
,
需要保证 panic -> recover -> panic -> recover ...
这样的链式关系。
package main
import "fmt"
func foo() {
defer func() {
println("recover 1")
if err := recover(); err != nil { // 可以捕获到 panic
fmt.Printf("[1] recovered %d\n", err)
}
}()
defer func() {
println("recover 2")
if err := recover(); err != nil { // 可以捕获到 panic
fmt.Printf("[2] recovered %d\n", err)
panic(err) // 捕获的同时继续抛出
}
}()
defer func() {
println("recover 3")
if err := recover(); err != nil { // 可以捕获到 panic
fmt.Printf("[3] recovered %d\n", err)
panic(err) // 捕获的同时继续抛出
}
}()
defer func() {
println("panic 1")
}()
defer func() {
println("panic 2")
}()
defer func() {
println("panic 3")
panic(3)
}()
}
func main() {
foo()
}
// $ go run main.go
// 输出如下
/**
panic 3
panic 2
panic 1
recover 3
[3] recovered 3
recover 2
[2] recovered 3
recover 1
[1] recovered 3
*/
小结 #
- 正确使用
defer
语句的地方是在成功获取资之后 os.Exit
会直接退出程序,不用调用已经注册的defer
函数recover
必须在defer
函数中调用且必须直接调用- 多个
panic
注册后,如果recover
, 那么只有 1 个panic
会被捕获