golang中的defer函数理解

 更新时间:2022年10月24日 16:11:58   作者:西京刀客  
defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源、关闭数据库连接、断开socket连接、解锁一个加锁的资源,这篇文章主要介绍了golang中的defer函数理解,需要的朋友可以参考下

golang的defer

什么是defer

defer的的官方文档:https://golang.org/ref/spec#Defer_statements

go语言中defer可以完成延迟功能,当前函数执行完成后再执行defer的代码块。通过defer,我们可以在代码中优雅的关闭/清理代码中所使用的变量。

defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源、关闭数据库连接、断开socket连接、解锁一个加锁的资源。

Go语言机制担保一定会执行defer语句中的代码。其它语言中也有类似的机制,比如Java、C#语言里的finally语句,C++语言里的析构函数(Destructor)可以起类似的作用,C++语言机制担保在对象被销毁前一定会执行析构函数中的代码。C++中的析构函数析构的是对象,Go中的defer析构的是函数。

理解defer

defer什么时间执行(defer、 return、返回值 三者的执行顺序)

defer只有在当前函数执行完毕后,才会执行。描述其实不太精确

go中的return语句并不是原子性操作,一般是分为两步:

  • 将返回值赋值给一个变量
  • 执行RET指令

return并不是原子性操作,是通过一个变量赋值和ret指令来完成的。defer就执行在1之后,2之前。defer的执行顺序在return之后,但是在返回值返回给调用方之前,所以使用defer可以达到修改返回值的目的。

defer、 return、返回值 三者的执行顺序是 : return 最先给返回值赋值;接着 defer 开始执行一些收尾工作;最后 RET 指令携带返回值退出函数。

package main

import (
    "fmt"
)

func main() {
    ret := test()
    fmt.Println("test return:", ret)
}
// func test() ( int) {  这种就是匿名返回值
//返回值改为命名返回值, 具名返回值。即返回值带有名字, 这样我们在执行defer的时候相当于修改了返回值的值
func test() (i int) {
    //var i int

    defer func() {
        i++
        fmt.Println("test defer, i = ", i)
    }()

    return i
}

注意: 这块验证使用了具名返回值 func test() (i int) { 中的(i int) 。 测试结果满足我们预期。

编码中,我们要特别注意, go语言中匿名返回值和命名返回值对defer的影响。不过一般我们都是使用命名返回值。

一个主函数拥有一个匿名的返回值,返回时使用字面值,比如返回”1”、”2”、”Hello”这样的值,这种情况下defer语句是无法操作返回值的。

defer输出的值,就是定义时的值。而不是defer真正执行时的变量值(注意引用情况)

defer函数会在return之后被调用。那么这段函数执行完之后,是不用应该输出1呢?

package main

import "fmt"

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

func main() {
	test1()
}

输出结果:0

虽然我们在defer后面定义的是一个带变量的函数: fmt.Println(i). 但这个变量(i)在defer被声明的时候,就已经确定其确定的值了。

总结: 因为defer后面的函数在入栈的时候保存的是入栈那一刻的值,而当时i的值是0,所以后期对i修改,并不会影响栈内函数的值。

我们再看下一个例子:

package main

import "fmt"


func test2() {
	x := 10
	defer func(a *int) {
		fmt.Println(*a)
	}(&x)
	x++
}

func main() {
	test2()
}

输出结果: 11

这里为什么和前面结论不一样呢?
这里defer后面函数入栈的时候存入的执行变量x的指针。所以,后期x值改变的时候,输出结果也会改变。

总结: 需要注意引用情况。对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,defer后面的语句对变量的修改会影响延迟函数。

多个defer,执行顺序

package main

import (
    "fmt"
)

func main() {
    defer fmt.Println("main defer1")
    test()
    defer fmt.Println("main defer2")
}

func test() () {
    defer func() {
        fmt.Println("test defer1")
    }()
    defer func() {
        fmt.Println("test defer2")
    }()
}

输出结果:

test defer2
test defer1
main defer2
main defer1

总结: 后进先出(LIFO)的顺序执行,即先出现的 defer 最后执行。即:多个defer语句的执行顺序是逆序执行。

defer的函数一定会执行么?

defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源、关闭数据库连接、断开socket连接、解锁一个加锁的资源。

Go语言机制担保一定会执行defer语句中的代码。其它语言中也有类似的机制,比如Java、C#语言里的finally语句,C++语言里的析构函数(Destructor)可以起类似的作用,C++语言机制担保在对象被销毁前一定会执行析构函数中的代码。C++中的析构函数析构的是对象,Go中的defer析构的是函数。

panic情况

网上demo:

package main

import (
	"fmt"
)

func test1() {
	fmt.Println("test")
}

func test2() {
	panic(1)
}
func main() {
	fmt.Println("main start")
	defer test1()
	test2() //造panic
	fmt.Println("main end")
}

执行结果:

main start
test                                                                  
panic: 1                                                                                                                                   
goroutine 1 [running]:                                                
main.test2(...)         

总结: 我们发现正常的panic,还是会调我们的defer的,并且在会在panic之前执行。

os.Exit情况

网上demo:

package main

import (
	"fmt"
	"os"
)

func test1() {
	fmt.Println("test")
}

func main() {
	fmt.Println("main start")
	defer test1()
	fmt.Println("main end")
	os.Exit(0)
}

执行结果:

main start
main end

总结: 如果在当前函数里是因为执行了os.Exit退出,而不是正常return退出或者panic退出,那程序会立即停止,被defer的函数调用不会执行。

kill情况(Ctrl+C)

package main

import (
	"fmt"
	"time"
)

func test1() {
	fmt.Println("test")
}

func test2() {
	time.Sleep(60 * time.Second)
}
func main() {
	fmt.Println("main start")
	defer test1()
	test2()
	fmt.Println("main end")
}

执行结果:

main start

Process finished with the exit code -1073741510 (0xC000013A: interrupted by Ctrl+C)

如上,我们test2() 睡眠时间内,点击Ctrl+C,发现defer test1()并没有执行。

总结:这个点很重要,需要我们在日常异常中断时,留意defer是否未处理的情况。
所以一般情况下,我们程序需要捕获这种异常中断,在程序退出前,手动做一些处理。

参考文献

Go常见坑:Go语言里被defer的函数一定会执行么?
参考URL: https://blog.csdn.net/perfumekristy/article/details/121343642
面试官:听说你精通golang的defer?
参考URL: https://cloud.tencent.com/developer/article/2076951

到此这篇关于golang中的defer函数理解的文章就介绍到这了,更多相关golang defer函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • go语言实现全排列的示例代码

    go语言实现全排列的示例代码

    本文主要介绍了go语言实现全排列的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • Go中strings包的基本使用示例代码

    Go中strings包的基本使用示例代码

    本文详细介绍了Go语言中strings包的基本使用方法,包括字符串的前缀、后缀判断,字符串包含、索引查找、字符串替换、计数、重复、大小写转换、修剪、分割、拼接以及数据类型转换等功能,示例代码丰富,适合初学者和需要使用字符串处理功能的开发者参考学习
    2024-10-10
  • 详解Golang中Context的三个常见应用场景

    详解Golang中Context的三个常见应用场景

    Golang context主要用于定义超时取消,取消后续操作,在不同操作中传递值。本文通过简单易懂的示例进行说明,感兴趣的可以了解一下
    2022-12-12
  • Go语言循环遍历含有中文的字符串的方法小结

    Go语言循环遍历含有中文的字符串的方法小结

    这篇文章主要介绍了Go语言循环遍历含有中文的字符串的几种方法,文章通过代码示例讲解的非常详细,具有一定的参考价值,感兴趣的小伙伴跟着小编一起来看看吧
    2023-07-07
  • go语言中切片与内存复制 memcpy 的实现操作

    go语言中切片与内存复制 memcpy 的实现操作

    这篇文章主要介绍了go语言中切片与内存复制 memcpy 的实现操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • 使用Go重试机制代码更可靠

    使用Go重试机制代码更可靠

    这篇文章主要为大家介绍了使用Go重试机制的使用,使你的代码更加可靠,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • 夯实Golang基础之数据类型梳理汇总

    夯实Golang基础之数据类型梳理汇总

    这篇文章主要8为大家介绍了夯实Golang基础之数据类型梳理汇总,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2023-10-10
  • Go语言学习笔记之文件读写操作详解

    Go语言学习笔记之文件读写操作详解

    这篇文章主要为大家详细介绍了Go语言对文件进行读写操作的方法,文中的示例代码讲解详细,对我们学习Go语言有一定的帮助,需要的可以参考一下
    2022-05-05
  • Go语言流程控制详情

    Go语言流程控制详情

    这篇文章主要介绍了Go语言流程控制详情,流程控制包含分三大类:条件判断,循环控制和无条件跳转。下面关于更多相关内容需要的小伙伴可以参考一下
    2022-03-03
  • 一文带你了解Go语言标准库strings的常用函数和方法

    一文带你了解Go语言标准库strings的常用函数和方法

    strings 库包含了许多高效的字符串常用操作的函数和方法,巧用这些函数与方法,能极大的提高我们程序的性能。本文就来和大家分享一下Go标准库strings的常用函数和方法,希望对大家有所帮助
    2022-11-11

最新评论