延迟调用
关键字 defer
在上一章中,代码里出现了defer
关键字
defer特性:
- 关键字 defer 用于注册延迟调用。
- 这些调用直到 return 前才被执行。因此,可以用来做资源清理。
- 多个defer语句,按先进后出的方式执行。
- defer语句中的变量,在defer声明时就决定了。
defer用途:
- 关闭文件句柄
- 锁资源释放
- 数据库连接释放
- 确保指定代码的执行
使用场景demo:
func test() error {
f, err := os.Create("test.txt")
if err != nil { return err }
defer f.Close() //延迟调用,关闭资源
f.WriteString("Hello, World!")
return nil
}
延迟执行次序
多个 defer
注册,按 FILO 次序执行(defer栈,先进后出)。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
func main() {
defer fmt.Print("a")
defer fmt.Print("b")
defer fmt.Print("c")
}
/*
输出cba
*/
延迟调用中变参与闭包值的问题
延迟调用参数在注册时求值或复制,可用指针或闭包 "延迟" 读取。(此处在上一章提到)
package main
import "fmt"
func test() {
x, y := 10, 20
defer func(i int) {
println("defer:", i, y) // y 闭包引用指针
}(x) // x 被复制
x += 10
y += 100
fmt.Println("x =", x, "y =", y)
}
func main() {
test()
}
/*
输出:
x = 20 y = 120
defer: 10 120
*/
因此就会出现下面的现象
package main
import "fmt"
func main() {
var whatever [5]struct{}
for i := range whatever {
defer func() { fmt.Print(i) }()//执行时i已经是4了,所以输出的都是4
}
}
/*
输出:44444
*/
不要滥用defer
滥用 defer
可能会导致性能问题,尤其是在一个 "大循环" 里。(defer涉及线程同步问题,降低运行效率)
错误处理
错误的抛出与捕获
没有结构化异常,使用 panic
抛出错误,recover
捕获错误。
func test() {
defer func() {//把此处的defer理解为java中的finally
if err := recover(); err != nil {//捕获错误,并判断错误是否为空
str := err.(string) // 将 interface{} 转型为具体类型。
println(str)
}
}()
panic("panic error!")
}
由于 panic
、recover
参数类型为 interface{}
,因此可抛出任何类型对象。
//panic 和 recover的声明
func panic(v interface{})
func recover() interface{}
recover() 中需要注意的点
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test() //输出:defer panic
}
捕获函数 recover
只有在延迟调用内直接调用才会终止错误,否则总是返回 nil
。任何未捕获的错误都会沿调用堆栈向外传递(类似java)。
//因此这样的调用是无效的
defer recover() //defer should not call recover() directly
defer fmt.Println(recover()) //同上
defer func(){ // 闭包层级过多?存疑
func() {
recover()
}
}
//使用延迟匿名函数是有效的。
defer func(){
recover()
}
//将问题处理函数提取也是有效的
func except() {
recover()
}
func test() {
defer except()
panic("test panic")
}
保护代码片段
如果需要保护代码片段,可将代码块重构成匿名函数,如此可确保后续代码被执行。
func test() {
//code
func() {
defer func() {
if recover() != nil {
fmt.Println("no boom!")
}
}()
fmt.Println("it will boom!")
panic("boom!")
}()
//后续代码能顺利被执行
fmt.Println("i am safe!")
}
//it will boom!
//no boom!
//i am safe!
Go实现类似 try catch 的异常处理
package main
import "fmt"
func Try(fun func(), handler func(interface{})) {
defer func() {
if err := recover(); err != nil {
handler(err)
}
}()
fun()
}
func main() {
Try(func() {
panic("test panic")
}, func(err interface{}) {
fmt.Println(err)
})
}
panic 与 error
除用 panic
引发中断性错误外,还可返回 error
类型错误对象来表示函数调用状态。
//error的接口
type error interface {
Error() string
}
似乎看起来error
与panic
功能上有些重复,多余了
如何区别使用 panic 和 error 两种方式?
惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。
标准库 errors.New
和 fmt.Errorf
函数用于创建实现 error
接口的错误对象。
func New(text string) error
func Errorf(format string, a ...interface{}) error
通过判断错误对象实例来确定具体错误类型。
package main
import (
"errors"
"fmt"
)
var ErrDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 {
return 0, ErrDivByZero
}
return x / y, nil
}
func main() {
defer func(){
if eil:=recover();eil!=nil{
//处理
}
}()
switch z, err := div(10, 0); err {
case nil:
fmt.Println(z)
case ErrDivByZero:
panic(err)//将error包装为一个panic,方便统一处理
}
}
参考
Q.E.D.