why defer
defer语句能延迟函数的执行,直到周围的函数return。在业务代码流程中,无论函数逻辑多复杂,开发者使用defer字段,
都可以保证在任何执行路径下,资源被释放,可以不关心语句何时结束,只需要在资源正常打开后,加上defer关闭语句即可。
defer的特性
1. 可用于回收资源
// io 资源的释放
f, _ := os.Open(fName)
defer f.Close()
// 锁的释放
var mutex sync.Mutex
...
mutex.Lock()
defer mutex.UnLock()
2. 可用于panic recover
需要注意的是,recover只能在defer函数中使用,且必须与出发panic的协程位于同一个协程。如果panic在不同协程中,recover是无法恢复的。
如下例子
defer func() {
if r := recover(); r != nil {
fmt.Println("recover from ", r)
}
}()
panic("error")
3. defer的参数会在执行前确定
这个特性可以用于制作计时器,计算程序执行时间。
如下例:
func a() time.Time {
curTime := time.Now()
return curTime
}
func b(beginTime time.Time) {
beginTimeStr := beginTime.Format("2006-01-02 15:04:05")
fmt.Println("a start time: " + beginTimeStr)
endTime := time.Now()
endTimeStr := endTime.Format("2006-01-02 15:04:05")
fmt.Println("b end time: " + endTimeStr)
}
func TestDefer1(t *testing.T) {
defer b(a())
time.Sleep(time.Second)
}
// 结果
//a start time: 2022-07-08 21:00:25
//b end time: 2022-07-08 21:00:26
4. 多个defer的执行顺序为先入后出(FILO)
多个defer执行时,先声明的defer最后才会执行。
如下例:
func deferA() {
fmt.Println("deferA")
}
func deferB() {
fmt.Println("deferB")
}
func deferC() {
fmt.Println("deferC")
}
func TestDefer2(t *testing.T) {
defer deferA()
defer deferB()
defer deferC()
}
//结果
//deferC
//deferB
//deferA
5.执行defer语句时,函数参数会被计算然后封装压栈,当defer被触发时,所有压栈的方法出栈并执行
如下例子中,a初始值为0,所以defer被压栈的值为0,而不是return时的3,在defer函数中执行的是func(0):
func main() {
fmt.Println("final value of a is ", f2(0))
}
func f2() (a int) {
defer func(a int) {
a += 5
fmt.Println("defer a = ", a)
}(a)
a += 3
return
}
//结果
//defer a = 5
//final value of a is 3
6. defer 对象可以是函数的函数
这个程序可以用在复杂程序调试,用于记录合适进入和退出函数。
func main() {
bigSlowOperation()
}
func bigSlowOperation() {
defer trace("bigSlowOperation")()
// ...lots of work…
time.Sleep(10 * time.Second)
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() {
log.Printf("exit %s (%s)", msg, time.Since(start))
}
}
7. 注意defer和return的先后顺序
return分三步
(1) 先执行return后面语句
(2) 接着defer中的函数运行
(3) 将returnValue move到结果输出的寄存器,携带返回值returnValue
退出
这里值得注意的是,在匿名返回值和命名返回值中,函数有不同表现。
我们可以观察一下代码例子:
func main() {
fmt.Println(testLocalVariableDefer())
fmt.Println(testParameterDefer(1))
}
// 匿名返回值
func testLocalVariableDefer() int {
//returnValue=value=0在defer前被创建
var value int
//defer中的修改是对value执行的,而不是returnValue
defer func() {
value++
}()
//defer结束,return的依然是最初声明的returnValue,即0
return value
}
// 命名返回值
func testParameterDefer(value int) int {
//returnValue在方法定义时已经被定义,所以没有创建returnValue的过程,value就是retValue
//defer对于value的修改也会被直接返回,即resultValue会被加一。
defer func() {
value++
}()
//defer结束,返回returnValue=1
return value
}
//结果
//0
//1
这里笔者建议,最好不要在defer中修改return的值,
如有业务需求(如对请求返回结果进行包装),尽量使用命名返回值
使用匿名返回值的话,没什么卵用,还会造成代码逻辑不清晰。
8.os.Exit()后不会执行defer
官方解释如下
// Exit causes the current program to exit with the given status code.
// Conventionally, code zero indicates success, non-zero an error.
// The program terminates immediately; deferred functions are not run.
9.在有错误抛出时,在错误处理后再使用defer
这里defer要写在err判断的后边而不是可能抛出错误的语句之前。理由很简单,如果资源没有获取成功,就没有必要对资源执行释放操作,直接返回就好了。而且,如果err不为nil就执行资源释放,可能导致Panic。
func openFuncWrong() {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
}
func openFuncCorrect() {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
}
10.不要在循环中用defer
以下是错误示范,defer会对其后需要的参数进行内存拷贝,还需要对defer结构进行压栈出栈操作。所以在循环中定义defer可能导致大量的资源开销。
for i := 0; i < 100; i++ {
f, _ := os.Open("/etc/hosts")
defer f.Close()
}
正确的做法是
- 在循环中用完资源就close,不要等整个函数结束后再一股脑调用堆栈中的defer
- 如果可以,尽量在循环结束后defer