1.defer是什么
官方解释:
A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.
- defer语句调用一个函数,该函数的执行延迟到defer语句所处函数return之后再执行
- defer、return、返回值三者的执行逻辑应该是:return最先执行,负责将结果写入返回值中;接着defer开始执行;最后函数携带当前返回值退出
- 相应的goroutine发生了panic也会触发defer的执行
2.为什么要defer
延迟执行可以用在很多的场景,比如连接数据库、打开文件、获取http连接等资源后,都需要释放资源,但是写代码的人容易忘记关闭资源的连接,且容易造成代码冗余。所以可以用defer语句在资源打开后马上调用defer去释放资源,可以避免忘记释放资源。因此,在诸如打开连接/关闭连接;申请/释放锁;打开文件/关闭文件等成对出现的操作场景里,defer会显得格外方便,如下:
res, err := http.Get(url)
if err != nil {
panic(err)
}
defer res.Body.Close()
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close()
3.怎么使用defer
1.单个defer执行顺序
func main() {
fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
}
//结果
1
3
2
可见加了defer的执行语句会被延迟到最后
2.多个defer执行顺序
func main() {
fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
defer fmt.Println("4")
fmt.Println("5")
}
//结果
1
3
5
4
2
从这个例子可以很清楚看到,加了defer的语句会被放到一个栈中,当所以没有加defer的语句执行完后,才会开始执行栈里的语句,所以顺序是1、2入栈、3、4入栈、5、4出栈、2出栈
3.defer执行+值传递
现在来看一下在defer语句后修改同一数据,最后输出的数据是否会受到影响
func main() {
a := 1
defer fmt.Println("defer", a)
a++
}
//结果
defer 1
根据前面介绍的defer会在return之后再执行,为什么还是打印1呢,原因是defer函数在defer语句执行那一刻就已经确定下来了,即此时要打印什么值已经确定好了,后面再修改值不会生效
同样的道理
func def(b int) {
fmt.Println("defer", b)
}
func main() {
a := 1
defer def(a)
a++
}
//结果
defer 1
可见输出的是1而不是2,因为使用defer的那一刻就已经确定a的值了,而且def内发生的是值传递,不会改变最后的结果,把地址打印出来看看
func def(b int) {
fmt.Println("--2", &b)
fmt.Println("defer", b)
}
func main() {
a := 1
fmt.Println("--1", &a)
defer def(a)
a++
}
//结果
--1 0xc000018080
--2 0xc0000180a0
defer 1
可见地址都不一样,a++作用的是0xc000018080地址上的值,而打印的是b的0xc0000180a0地址上的值,最终的结果肯定不可能是2
4.defer执行+指针传递
接着上面所说,如果函数中传递的是指针类型的数据呢?
func def(b *int) {
fmt.Println("defer", *b)
}
func main() {
a := 1
defer def(&a)
a++
}
//结果
defer 2
此时的结果为2,而不是1,为什么呢?因为此时给def函数传递的是a的地址,a++的执行是把a的地址上的值+1,然后把经过+1后的a的地址上的值赋值给b,最后defer打印出来的值是a的地址上的值经过+1后的值,所以最后的结果为2,现在试着把地址打印出来
func def(b *int) {
fmt.Println("==2", b)
fmt.Println("defer", *b)
}
func main() {
a := 1
fmt.Println("==1", &a)
defer def(&a)
a++
fmt.Println("==3", &a)
}
//结果
==1 0xc000018080
==3 0xc000018080
==2 0xc000018080
defer 2
最终证实了上述的解释
5.defer执行+闭包
func main() {
a := 1
defer func() {
fmt.Println(a)
}()
a++
}
//结果
2
这次什么值都没有传递,为什么打印出来的却是2呢?原因是使用了闭包结构,先把地址打印出来看看
func main() {
a := 1
fmt.Println("--1", &a)
defer func() { //闭包
fmt.Println("--2", &a)
fmt.Println(a)
}()
a++
}
//结果
--1 0xc000018080
--2 0xc000018080
2
可见地址是一样的,最终打印的值是同一地址上经过+1的值
这里可以简单解释一下闭包的作用
- 可以读取函数内部的变量
- 闭包里的变量本质上是对上层变量的引用,因此最后的值就是引用的值
- 让这些变量的值始终保持在内存中,不会被GC
6.defer执行+非命名返回值
func def1() int {
a := 1
defer func() {
a++
}()
return a
}
func main() {
fmt.Println("def1", def1())
}
//结果
def1 1
defer中的a++并没有影响到最终的结果
func def2() *int {
a := new(int)
*a = 1
b := a
defer func() {
*b++
}()
return b
}
func main() {
fmt.Println("def1", *def2())
}
//结果
defer 2
这里返回2还收因为指针传递的缘故
上面两个案例返回值都没有显示命名
7.defer执行+命名返回值
现在来试一下显示命名会发生什么
func def1() (a int) { //显示命名返回值
a = 1
defer func() {
a++
}()
return a
}
func main() {
fmt.Println("def1", def1())
}
//结果
def1 2
func def2() (a *int) { //显示命名返回值
b := new(int)
*b = 1
a = b
defer func() {
*a++
}()
return a
}
func main() {
fmt.Println("def1", *def2())
}
//结果
defer 2
可见,两个案例都返回的2,和第6小节对比后,发现没用指针传递的函数结果非命名返回值的是1,命名返回值的是2,而用了指针传递的函数两个结果都是2
为什么会这样呢,因为return时会重新把要返回的结果赋值给另一个变量,那么defer里面的+1操作是对赋值前的变量进行+1,最终返回的结果并没有+a,而使用指针传递或显示命名返回值,执行的+1操作是对相同地址上的值+1或最终要返回的值+1,所以才会造成这种差异
8.defer+recover+panic
defer的一个常用场景就是配合rcover对panic的处理,因为不知道什么时候会发生panic,直接在最开头用如下代码以防万一
func main() {
defer func() {
if err := recover(); err != nil {
log.Println(err)
}
}()
panic(errors.New("报错"))
}
//结果
2022/01/17 17:03:59 报错
如果没有用recover,直接会退出程序,并报错
panic: 报错
goroutine 1 [running]:
main.main()
/Users/pp/kevin_go/src/go_learning/test.go:14 +0x65
所以defer+recover可以用于防止程序直接退出,较多用在goroutine并发中,当一个协程panic后不会影响到其他协程,可以让程序继续执行
如果在pacin之前有其他defer调用会怎么打印呢?
func Def() {
defer func() { fmt.Println("1") }()
defer func() { fmt.Println("2") }()
defer func() { fmt.Println("3") }()
panic("异常")
}
func main() {
Def()
}
结果
3
2
1
panic: 异常
goroutine 1 [running]:
main.Def()
/Users/pp/kevin_go/src/go_learning/test.go:12 +0x6b
main.main()
/Users/pp/kevin_go/src/go_learning/test.go:16 +0x17
可见在panic前会先执行完defer的调用
还有一个地方容易出现陷阱,就是recover必须要被defer直接调用,看下面两个例子
func getRecover() {
if err := recover(); err != nil {
fmt.Println("报错")
}
}
func main(n int) {
defer getRecover()
//后续操作
}
func getRecover() bool {
if err := recover(); err != nil {
fmt.Println("报错")
return true
}
return false
}
func Update() {
defer func() {
if IsPanic() {
//回滚
} else {
//提交
}
}()
//后续数据库操作
}
第一个案例是可以正常捕获到panic的,但是第二个案例却失效了,有如下三种情况会让recover返回nil而导致err为nil
- panic时没有打印(一般是panic(“err”))
- 没有发生panic
- recover没有被defer方法直接调用
第二个案例就是因为发生了第三种情况而导致recover永远返回nil
9.for+go+defer
for中使用defer要尤为注意
func Def() {
for i := 0; i<3; i++ {
defer fmt.Println(i)
}
}
func main() {
Def()
}
//结果
2
1
0
上面的代码for是限定了范围的,所以总能执行完毕,如果没有限定范围呢
var i int
func Def() {
for {
i++
defer fmt.Println(i)
}
}
func main() {
Def()
}
上面这个代码永远不会有结果,因为defer时会把语句放入栈中,当for结束时一起出栈,但是for{}因为没有限定范围,所以永远不会出栈,即形成了死循环,申请的内存得不到释放,然后会导致程序占满整个内存,死机
那有什么办法让defer能在for{}中执行呢,可以用goroutine匿名函数的方式调用
var i int
func Def() {
for {
go func() {
i++
defer fmt.Println(i)
}()
//这里要加上间隔时间,不然goroutine会创建过快导致程序卡死
time.Sleep(time.Millisecond*500)
}
}
func main() {
Def()
}
这里的defer会在匿名函数结束的时候得到执行,就不会出现之前的资源没有释放的情况
4.defer性能消耗
使用defer调用函数会比直接调用函数开销更大,测试一下
func def1() (a int) {
a = 1
defer func() {
a++
}()
return a
}
func def2() (a int) {
a = 1
func() {
a++
}()
return a
}
func BenchmarkDefer1(b *testing.B) {
for i := 0; i < b.N; i++ {
def1()
}
}
func BenchmarkDefer2(b *testing.B) {
for i := 0; i < b.N; i++ {
def2()
}
}
结果如下
goos: darwin
goarch: amd64
pkg: GO_Learning/01_base/08_test
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkDefer1
BenchmarkDefer1-8 439356783 2.623 ns/op
BenchmarkDefer2
BenchmarkDefer2-8 1000000000 0.3057 ns/op
PASS
进程 已完成,退出代码为 0
可见使用了defer后性能比不使用defer下降不少,每个操作所花费的时间为 2.623,而不使用defer则快多了 0.3057 ns/op
5.参考链接