defer 延迟调用函数

为什么需要defer语句?

func readFileWithoutDefer(filename string) {
file, err := os.Open(filename)
if err != nil {
fmt.Println("Error opening file:", err)
return
}

// 使用文件进行操作
fmt.Println("Reading file:", filename)

// 关闭文件(必须显式调用)
err = file.Close()
if err != nil {
fmt.Println("Error closing file:", err)
}
}

readFileWithoutDefer 中需要在任何执行路径下都正确关闭文件,当函数逻辑边的复杂时,需要处理的错误情况越来越多,这时简单的清理操作就会造成之后的维护问题。

defer 语句是一个普通的函数或方法调用,只不过要在调用之前,加上defer关键字,其将函数的调用推迟,在 defer 调用的函数中执行清理操作来简化实现负,担无论包含defer语句的函数是正常退出,还是不正常,退出比如触发 panic 。都会再函数结束后,调用 defer 语句。

defer没有次数限制,执行时按照调用defer顺序的倒序执行

defer 中值的确定

通常使用闭包来写defer,这就涉及到defer执行的时候是怎么确定里面的值的,主要有以下原则

  • 作为参数传入:定义defer时确定
  • 作为闭包引入:执行defer时确定
// 作为参数传入:定义defer时确定
func deferClosure() {
i := 0
defer func() {
// 输出1
println(i)
}()
i = 1
}

// 作为闭包引入:执行defer时确定
func deferArgs() {
i := 0
defer func(val int) {
// 输出0
println(val)
}(i)
i = 1
}

使用场景

资源释放、错误处理

defer 通常成对出现:连接建立和关闭,锁的获取和释放,文件的打开和关闭

调试函数

defer 还可以用来调试复杂函数,统计函数的运行时间,这段代码很巧妙,引用自《Go程序设计语言》

func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() { log.Printf("exit %s cost %v", msg, time.Since(start)) }
}

func bigSlowOp() {
defer trace("bigSlowOp")()
time.Sleep(time.Second * 10)
}

修改返回值

在defer中只能修改具名返回值,如果是指针,则可以修改指针指向的值

// DeferReturn 闭包引用
func DeferReturn() int {
a := 0
log.Printf("func a addr : %v", &a)
defer func() {
a = 1
log.Printf("func a addr : %v", &a)
}()
return a
}

// DeferReturnV1 具名返回值
func DeferReturnV1() (a int) {
a = 0
defer func() {
a = 1
}()
return a
}

// DeferReturnV2 闭包引用指针
func DeferReturnV2() *int {
a := new(int)
defer func() {
*a = 1
}()
return a
}
func TestDeferReturn(t *testing.T) {
log.Printf("%v\n", week1.DeferReturn())
log.Printf("%v\n", week1.DeferReturnV1())
log.Printf("%v\n", *week1.DeferReturnV2())
}

defer-modify-return

DeferReturn()可以看到,即使地址一样,闭包引入的值不能修改;
DeferReturnV2()中的代码,演示了可以通过修改指针指向的值来修改,但本质上a是无法修改的

实现机制

堆上分配

整个defer直接分配到堆上
缺点:被GC管理

go-defer-heap

栈上分配

整个defer分配到goroutine上,不需要被GC管理,相对于分配在栈上,性能提升了30%,而defer具体在堆上分配还是在栈上分配,取决于逃逸分析。

go-defer-stack

开放编码

启用内联的优化,直观上理解就是把defer内容放到函数的最后,编译器帮我把编码转移到了函数的最后

启用条件:

  1. defer的数量 <= 8; 用了一个byte来记录哪些defer需要执行
  2. defer关键字不能再循环中执行:;编译时不知道有多少个defer
  3. return语句个数 * defer个数 <= 15;硬性规定