浅析golang中的defer

举报
苏州程序大白 发表于 2022/03/21 14:13:58 2022/03/21
【摘要】 浅析golang中defer的用法和注意事项1.defer是什么官方解释:A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function execu...

浅析golang中defer的用法和注意事项

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

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。