在go语言中,为什么使用defer? defer 为何能实现延迟加载?

发布时间 2023-06-06 09:50:28作者: 李若盛开

defer会在函数返回前执行,那么我把代码,例如资源释放等,写到函数末尾不就行了,为什么使用defer?什么情况下必须使用defer,它有什么好处呢?

在使用defer的场景中,主要有以下几个情况:

 

1. 关闭资源:使用defer可以确保在函数执行完毕后关闭打开的文件、网络连接、数据库连接等资源。如果没有使用defer,那么可能会因为依赖外部代码调用顺序而导致资源未被正确释放,从而导致内存泄漏或其他问题。

 

2. 错误处理:当代码发生异常错误,需要进行一些清理工作时,使用defer可以确保这些清理工作会在函数返回前执行。这有助于在异常情况下避免资源泄露和数据一致性问题。

 

3. 记录日志:在函数结束时记录日志是一个常见的行为。使用defer可以让日志写入操作在函数返回前完成,确保日志记录完整。

 

好处:

 

1. 简化代码:使用defer可以将一些常规的清理任务从代码逻辑中分离出来,从而使得业务逻辑更加简洁、易读、易维护,并且能够提高代码的可重用¥。

 

2. 防止遗漏:使用defer能够确保即便出现异常,也会执行必要的清理操作,避免了由于遗漏清理代码而导致的资源泄露等问题。

 

总而言之,使用defer可以降低代码复杂度、提高可读性、避免代码逻辑错误,是一个非常实用的语言特性。


defer含义

  • defer是延迟的意思,在Go里可以放在某个函数或者方法调用的前面,让该函数或方法延迟执行
  • 语法:
defer function([parameter_list]) // 延迟执行函数
defer method([parameter_list]) // 延迟执行方法
  • defer本身是在某个函数体内执行,比如在函数A内调用了defer func_name(),只要defer func_name()这行代码被执行到了,那func_name这个函数就会被延迟到函数A return或者panic之前执行
    注意:如果是函数是因为调用了os.Exit()而退出,那defer就不会被执行了。参见Go语言里被defer的函数一定会执行么?
defer func_name([parameter_list])
defer package_name.func_name([parameter_list]) // 例如defer fmt.Println("blabla")
  • 如果在函数内调用了多次defer,那在函数return之前,defer的函数调用满足LIFO原则,先defer的函数后执行,后defer的函数先执行。比如在函数A内先后执行了defer f1(), defer f2(), defer f3(),那函数A return之前,会按照f3(), f2(), f1()的顺序执行,再return。

defer的用途?

Answer:defer常用于成对的操作,比如文件打开后要关闭、锁的申请和释放、sync.WaitGroup跟踪的goroutine的计数器的释放等。为了确保资源被释放,可以结合defer一起使用,避免在代码的各种条件分支里去释放资源,容易遗漏和出错。

  • 示例1
package main
import (
 "fmt"
 "sync"
)
var wg sync.WaitGroup
func sumN(N int) {
 // 调用defer wg.Done()确保sumN执行完之后,可以对wg的计数器减1
 defer wg.Done()
 sum := 0
 for i:=1; i<=N; i++ {
  sum += i
 }
 fmt.Printf("sum from 1 to %d is %d\n", N, sum)
}
func main() {
 // 设置wg跟踪的计数器数量为1
 wg.Add(1)
 // 开启sumN这个goroutine去计算1到100的和
 go sumN(100)
 // Wait会一直等待,直到wg的计数器为0
 wg.Wait()
 
 fmt.Println("finish")  
}
package main
import (
 "fmt"
 "sync"
)
func worker(id int) {
  fmt.Println(id)
}
func main() {
 var wg sync.WaitGroup
 size := 10
 wg.Add(size)
 
 for i:=0; i<size; i++ {
   i := i 
   /*把worker的调用和defer放在一个闭包里
   这样worker函数内部就不用使用WaitGroup了
   */
   go func() {
     defer wg.Done()
     worker(i)
   }()
 }
 wg.Wait()
}

注意事项

  • defer后面跟的必须是函数或者方法调用,defer后面的表达式不能加括号。
defer (fmt.Println(1)) // 编译报错,因为defer后面跟的表达式不能加括号
  • 被defer的函数的参数在执行到defer语句的时候就被确定下来了。
func a() {
 i := 0
 defer fmt.Println(i) // 最终打印0
 i++
 return
}

上例中,被defer的函数fmt.Println的参数i在执行到defer这一行的时候,i的值是0,fmt.Println的参数就被确定下来是0了,因此最终打印的结果是0,而不是1。

  • 被defer的函数执行顺序满足LIFO原则,后defer的先执行。
func b() {
 for i := 0; i < 4; i++ {
   defer fmt.Print(i)
 }
}

上例中,输出的结果是3210,后defer的先执行。

  • 被defer的函数可以对defer语句所在的函数的命名返回值做读取和修改操作。
// f returns 42
func f() (result int) {
 defer func() {
  // result is accessed after it was set to 6 by the return statement
  result *= 7
 }()
 return 6
}

上例中,被defer的函数func对defer语句所在的函数f的命名返回值result做了修改操作。
调用函数f,返回的结果是42。
执行顺序是函数f先把要返回的值6赋值给result,然后执行被defer的函数func,result被修改为42,然后函数f返回result,也就是返回了42。

 
链接:https://www.zhihu.com/question/457166084