Golang详解go语言中的defer

Posted CJ-cooper

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang详解go语言中的defer相关的知识,希望对你有一定的参考价值。

defer


用于延迟函数的调用,每次defer都会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行

延迟函数的参数在defer语句出现时就已经确定下来了

如:

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

defer语句中的fmt.Println()参数i值在defer出现时就已经确定下来,实际上是拷贝了一份。后面对变量i的修改不会影响fmt.Println()函数的执行,仍然打印”0”。但是注意 这里对于指针类型参数,仍然适用,所以defer后面的语句对变量修改还是会影响延迟函数如:

func a() {
	i := []int{1,2,3}
	defer fmt.Println(i)
	i[0]=10
	return
}//会输出[10,2,3],因为这里i为指针,是数组的地址

先进后出

很好理解,延迟函数执行按先进后出顺序执行,即先进去的defer最后执行

延迟函数可能操作主函数的具名返回值

defer可能会改变返回值,因为return语句不是原子的,实际执行为设置返回值-->ret

defer语句实际执行在返回前,即拥有defer的函数返回过程是:设置返回值–>执行defer–>ret。

注意:这里返回值必须要有具体名字

如下:

func deferFuncReturn() (result int) {
    i := 1

    defer func() {
       result++
    }()

    return i
}

输出2,return语句先把result设置为i的值,即1,defer语句又把result递增1,所以最终返回2

如果返回的是匿名变量,则不改变,如:

func foo() int {
	var i int

	i=1
	defer func() {
		i++
	}()

	return i
}

这里还是返回1,上面的返回语句可以拆分成以下过程:

t=i	//t就是要返回的值
i++
return //返回t,这里t还是1

defer 的执行顺序是后进先出。当出现 panic 语句的时候,会先按照 defer 的后进先出的顺序执行,最后才会执行panic

Golang Go语言中的 defer 怎么使用?

01

介绍

在 Golang 语言中,我们可以在函数(自定义和部分内置)或方法中使用 defer 关键字注册延迟调用(一个或多个),多个延迟调用的执行顺序是先进后出(FILO)。并且不会受到函数执行结束退出,显式调用 return 和主动(或被动)触发 panic 的影响,注册成功的所有延迟调用都会被执行,除非 defer 注册在 return 之后或者函数(或方法)调用 os.Exit(1)

defer 注册多个延迟调用,执行顺序是先进后出(FILO)。

示例代码:

func main () {
 defer func() {
  fmt.Println("A")
 }()

 defer func() {
  fmt.Println("B")
 }()

 fmt.Println("main goroutine run over")

 // panic("this is a panic example")

 // return
}

defer 如果定义在 return 之后,它等于 defer 没有注册,将不会执行。

示例代码:

func main () {
 fmt.Println("main")
 return
 defer func() {
  fmt.Println("A")
 }()
}

defer 所在的函数或方法中,如果调用 os.Exit(1),defer 即便注册,也不会执行。

示例代码:

func main () {
 defer func() {
  fmt.Println("A")
 }()
 fmt.Println("main")
 os.Exit(1)
}

defer 必须在函数和方法中才可以使用,并且 defer 后面必须是函数(自定义和部分内置函数)或方法,defer 函数的实参是值拷贝。

示例代码

func main () {
 a := 0
 defer func(num int) {
  fmt.Println("defer func()", num)
 }(a)
 a++
 fmt.Println(a)
}

02

使用场景

使用关键字 defer 注册的函数(自定义和部分内置)或方法,因为不会受到函数执行结束,显式调用 return 和主动或被动)触发 panic 的影响,通常会用于防止忘记释放资源和捕获 panic(同一 goroutine 中) 防止应用程序崩溃退出的应用场景。

示例代码:

func main () {
 f, err := os.OpenFile("text.txt", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0755)
 if err != nil {
  fmt.Println(err)
 }
 defer f.Close()
 n, err := f.WriteString("this is a text file\t")
 if err != nil {
  fmt.Println(err)
 }
 fmt.Println(n)
}

阅读上面这段代码,我们使用 defer 延迟调用释放资源,防止忘记释放资源(关闭文件或解锁),通常 defer 会放在错误检查之后。

示例代码:

func main () {
 defer func() {
  if err := recover(); err != nil {
   fmt.Println("this is a panic" )
  }
 }()
 panic("this is a test panic")
 fmt.Println("main")
}

阅读上面这段代码,我们使用 defer 配合 recover 函数,用于拦截 panic(同一 goroutine 中),防止程序崩溃退出。

03

注意事项

虽然使用 defer 具有可以用于防止忘记释放资源和拦截 panic(同一 goroutine 中)防止应用程序崩溃退出等好处。

但是 defer 也有副作用,它会使资源延迟释放,defer 尽量不要再 for-loop 中使用,并且相比于未使用 defer 调用的函数(自定义和部分内置)或方法,defer 也有一定的性能损耗,Golang 语言官方也在 golang 1.13 和 golang 1.14 中优化了 defer 的性能。

相比于 defer 的性能损耗,defer 带来的使代码更加优雅、可读和健壮等优势,我认为 defer 综合来看,利大于弊,它可以给 gopher 们带来的收益比付出的代价更大。所以,我建议大家尽量使用 defer。

还有一点需要注意的是,我们不要使用 defer 调用有返回值的自定义函数或方法,返回值会丢失,可能会给应用程序带来意想不到的错误。

04

总结

本文我们介绍了 defer 的执行机制,使用场景和注意事项,并且给出了相应的示例代码。通常我们会在 Golang 语言开发中使用 defer 防止忘记释放资源(关闭文件或解锁)和捕获 panic(同一 goroutine 中) 防止应用程序崩溃退出。

关于 defer 的原理分析,限于篇幅,并没有花费笔墨,感兴趣的读者朋友们,建议自行搜索相关资料了解相关内容。

参考资料:
https://gobyexample-cn.github.io/defer 
https://blog.golang.org/defer-panic-and-recover 

点赞关注,变得更强