golang踩坑--defer知多少

本文详细介绍了 Go 语言中的 `defer` 关键字,包括其在资源释放、 Panic 和 Recover 中的应用,以及参数求值、执行顺序和注意事项。强调了 `defer` 在错误处理、避免资源泄漏和函数退出顺序中的重要作用,并提供了最佳实践建议,如避免在循环中使用 `defer` 和正确处理返回值。
摘要由CSDN通过智能技术生成

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()
}

正确的做法是

  1. 在循环中用完资源就close,不要等整个函数结束后再一股脑调用堆栈中的defer
  2. 如果可以,尽量在循环结束后defer
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值