defer详解

106 阅读3分钟

defer的作用

go中的defer延迟函数,一般是用于释放资源或者收尾工作

由于defer是具有延迟特性且执行动作是在函数return之后,因此作为资源释放作用再好不过。

  • 典型例子:释放锁、关闭文件、关闭链接
// 释放锁
func getValue() {
  s.Lock()
  defer s.Unlock()
  ...
}
​
// 关闭文件
func read() {
  f, err := os.OpenFile("filename", os.O_RDWR, os.ModePerm)
  defer f.Close()
  ...
}
​
// 关闭链接
func connect() {
  resp, err := grequests.Get(s.cfg.GateApiCrossMarginUrl, nil)
  defer resp.Close()
  ...
}
​
// 收尾工作,defer 同时也是函数,可以做很多收尾相关工作
func closeConnection() {
  ...
  defer func() {
    file.close()
    close(readChan)
  }
  ...
}
  • 还有作用就是捕获 panic,这个功能在defer里也是典型用法
go
 代码解读
复制代码
func sendChan() {
  // 此处捕获 panic 进行 recover 防止程序崩溃
  defer func() {
    if ok := recover(); ok != nil {
        fmt.Println("recover")
    }
  }()
  // 向已经关闭的chan发送数据,此处会引起 panic
  dataChan <- "message"
  ...
}

资源释放动作一定紧跟资源使用(打开、连接)语句,不然defer可能不会被执行到,导致内存泄露

defer执行细节

执行顺序及对返回值的影响

func Run2() {
	fmt.Println("a return:", a()) // 打印结果为 a return: 0
	fmt.Println("b return:", b()) // 打印结果为 b return: 2
	fmt.Println("c return:", c()) // 打印结果为 c return: 2
}

func a() int {
	var i int
	defer func() {
		i++
		fmt.Println("a defer2:", i) // 打印结果为 a defer2: 2
	}()
	defer func() {
		i++
		fmt.Println("a defer1:", i) // 打印结果为 a defer1: 1
	}()
	return i
}

func b() (i int) {
	defer func() {
		i++
		fmt.Println("b defer2:", i) // 打印结果为 b defer2: 2
	}()
	defer func() {
		i++
		fmt.Println("b defer1:", i) // 打印结果为 b defer1: 1
	}()
	return i // 或者直接 return 效果相同
}

func c() (i int) {
	defer func() {
		i++
		fmt.Println("b defer2:", i) // 打印结果为 c defer2: 2
	}()
	defer func() {
		i++
		fmt.Println("b defer1:", i) // 打印结果为 c defer1: 1
	}()
	return 0 // 或者直接 return 效果相同
}

根据上面的例子可以看出:

  • 多个 defer 的执行顺序为“后进先出/先进后出”
  • ‍defer、return、返回值三者的执行顺序应该是:return最先给返回值赋值;接着 defer 开始执行一些收尾工作;最后 RET 指令携带返回值退出函数
  • 在返回变量被提前声明的情况下,return操作是将值赋给返回变量

defer函数传参

还需注意,defer声明时会先计算确定参数的值,defer推迟执行的仅是其函数体

func main() {
	defer P(time.Now())
	time.Sleep(5e9)
	fmt.Println("main ", time.Now())
}

func P(t time.Time) {
	fmt.Println("defer", t)
	fmt.Println("P    ", time.Now())
}

// main  2024-08-07 09:55:54.443061 +0800 CST m=+5.001461710
// defer 2024-08-07 09:55:49.441861 +0800 CST m=+0.000252501
// P     2024-08-07 09:55:54.443939 +0800 CST m=+5.002339376

defer panic

一般情况下,在程序里记录错误日志,就可以帮助我们在碰到异常时快速定位问题。

但还有一些错误比较严重的,比如数组越界访问,程序会主动调用 panic 来抛出异常,然后程序退出。

如果不想程序退出的话,可以使用 recover 函数来捕获并恢复。

  • defer 只对当前协程有效(main 可以看作是主协程);

  • 当任意一条(主)协程发生 panic 时,会执行当前协程中 panic 之前已声明的 defer;panic 会在调用栈上逐级传播,执行每一层的 defer 语句

  • 在发生 panic 的(主)协程中,如果 defer 语句中调用 recover 并成功恢复 panicpanic 的传播就会停止;如果没有一个 defer 调用 recover()进行恢复,则会在执行完最后一个已声明的 defer 后,引发整个进程崩溃;

  • 主动调用 os.Exit(int) 退出进程时,defer 将不再被执行。

func main() {
	fmt.Println("Start of main")
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered in main:", r)
		}
	}()
	f1()
	fmt.Println("End of main")
}

func f1() {
	fmt.Println("Start of f1")
	defer func() {
		fmt.Println("Deferred in f1")
		if r := recover(); r != nil {
			fmt.Println("Recovered in f1:", r)
		}
	}()
	f2()
	fmt.Println("End of f1")
}

func f2() {
	fmt.Println("Start of f2")
	defer func() {
		fmt.Println("Deferred in f2")
	}()
	panic("Panic in f2")
	fmt.Println("End of f2") // This line will not be executed
}

// Start of main
// Start of f1
// Start of f2
// Deferred in f2
// Deferred in f1
// Recovered in f1: Panic in f2
// End of main

引用参考文章: juejin.cn/post/684490…