延迟调用

关键字 defer

上一章中,代码里出现了defer关键字

defer特性:

  1. 关键字 defer 用于注册延迟调用。
  2. 这些调用直到 return 前才被执行。因此,可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。
  4. defer语句中的变量,在defer声明时就决定了。

defer用途:

  1. 关闭文件句柄
  2. 锁资源释放
  3. 数据库连接释放
  4. 确保指定代码的执行

使用场景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!")
}

由于 panicrecover 参数类型为 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
}

似乎看起来errorpanic功能上有些重复,多余了

如何区别使用 panic 和 error 两种方式?

惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。

标准库 errors.Newfmt.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,方便统一处理
	}
}

参考

Go 边看边练

golang·看云 延迟调用

gobyexample - panic

Q.E.D.


此 生 无 悔 恋 真 白 ,来 世 愿 入 樱 花 庄 。