我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Golang 的defer执行规则说明

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Golang 的defer执行规则说明

defer介绍

defer是golang的一个特色功能,被称为“延迟调用函数”。当外部函数返回后执行defer。类似于其他语言的 try… catch … finally… 中的finally,当然差别还是明显的。

在使用defer之前我们应该多了解defer的特性,这样才能避免使用上的误区。

1. 最简单的defer


func test(){
    defer func(){ fmt.Println("defer") }()
    //todo
    //...
    return
    //defer执行时机
}

我们可以稍微改动一下上述代码再次确认defer的执行时机。


func main() {
    fmt.Println(test())
}
func test() (i int) {
    defer func() { i++ }()
    defer func() { fmt.Println(i) }()
    //todo
    //...
    fmt.Println(0)
    return 1
    //defer执行时机
}

output:

0

1

2

从上面示例可以发现defer执行是在return之后,且按照defer声明的先进后出顺序执行。以下是真实场景中比较常见的用法。

释放占用的资源


func test() error {
    file, err := os.Open("path")
    if err != nil {
        return err
    }
    //放在判断err状态之后
    defer file.Close()
    //todo
    //...
    return nil
    //defer执行时机
}

捕捉处理异常


func test2() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    file, err := os.Open("path")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    //todo
    //...
    return
    //defer执行时机
}

输出日志 等收尾工作


func test3() {
    t1 := time.Now()
    defer func() {
        fmt.Printf("耗时: %f s", time.Now().Sub(t1).Seconds())
    }()
    //todo
    //...
    return
    //defer执行时机
}

2. 复杂的defer

当我们已经深深记住defer的执行时机并打算翻过这一页时,事情的发展又开始偏离初始。请看如下代码


func test() {
    i := 0
    defer fmt.Println(i)                       //输出 0
    defer func(x int) { fmt.Println(x) }(i)    //输出 0
    defer func(x *int) { fmt.Println(*x) }(&i) //输出 1
    defer func() { fmt.Println(i) }()          //输出 1
    i++
    //todo
    //...
    fmt.Println(i) //输出 1
    return   
}

output:

1

1

1

0 // 值未修改

0 // 值未修改

通常认为defer就像真的被挪到了return之后。

但defer的本质依然是函数调用。当执行到defer定义时,首先会对参数进行求值,然后参数被压入函数调用栈,此时不会进入defer函数体,而是直到函数返回时才调用defer函数体。

参数被压入函数调用栈时,如果参数是值类型,那么将复制值,如果参数是指针,那么将复制指针而不是复制指针指向的值。

defer函数体内的变量是在return后执行因此不受影响。

因此我们在使用defer时一定要明确函数的参数类型(如果有的话),其次要明确defer函数体内的变量引用是否正确。

以下是常见错误


func test4() error {
    f, err := os.Open("A.txt")
    if err != nil {
        return err
    }
    defer func() { f.Close() }()//错误: 关闭是B文件,f引用被重新赋值
    f, err = os.Open("B.txt")
    if err != nil {
        return err
    }
    defer func() { f.Close() }() //关闭是B文件
    list := []int{1, 2}
    for _, i := range list {
        defer fmt.Println(i) //输出 2 1 //i为值类型参数被复制
        defer func() { fmt.Println(i) }() //错误: 输出 2 2 //函数体内对i引用,留最终值
    }
    return nil
}

3. 更复杂点的defer

且看如下代码


type Test struct {
    name string
}
func (this *Test) Point() { // this  为指针
    fmt.Println(this.name)
}
func (this  Test) Value() { //this  为值类型 
    fmt.Println(this.name)
}
func test5() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        defer t.Point() //输出 c c c
        defer t.Value() //输出 c b a
    }
}

看似相同的代码却输出了完全不同的结果。要理解这种差别还是得从调用函数的本质来说。golang对struct 的方法调用是这样的


defer func (this Type, para) result

struct 的方法在定义时this 采用的值类型还是指针决定defer在调用时首个参数(隐藏)的复制的不同。

上述代码中Point方法定义时使用了指针作为this,因此输出是for循环最终赋值的t引用。

Value方法定义时使用值类型作为this,因此输出是for循环执行的每步复制后的t。

最终只有理解了以上出现的问题,到此defer所带来困扰才能远离我们。

补充:Golang中defer的三个实战要点

Golang中的defer是使用频次比较高的,能创造出延迟生效特效的一种方式。

defer也有自己的矫情,需要注意的。

本文将从通过代码的方式来说明defer的三点矫情。

1.defer的生效顺序

先说结论:defer的执行顺序是倒序执行(同入栈先进后出)


func main() {
 defer func() {
  fmt.Println("我后出来")
 }()
 defer func() {
  fmt.Println("我先出来")
 }()
}

执行后打印出:

我先出来

我后出来

2.defer与return,函数返回值之间的顺序

先说结论:return最先执行->return负责将结果写入返回值中->接着defer开始执行一些收尾工作->最后函数携带当前返回值退出

返回值的表达方式,我们知道根据是否提前声明有两种方式:一种是func test() int 另一种是 func test() (i int),所以两种情况都来说说


func test() int
func main() {
 fmt.Println("main:", test())
}
func test() int {
 var i int
 defer func() {
  i++
  fmt.Println("defer2的值:", i)
 }()
 defer func() {
  i++
  fmt.Println("defer1的值:", i)
 }()
 return i
}

输出:

defer1的值: 1

defer2的值: 2

main: 0

详解:return的时候已经先将返回值给定义下来了,就是0,由于i是在函数内部声明所以即使在defer中进行了++操作,也不会影响return的时候做的决定。


func test() (i int)
func main() {
 fmt.Println("main:", test())
}
func test() (i int) {
 defer func() {
  i++
  fmt.Println("defer2的值:", i)
 }()
 defer func() {
  i++
  fmt.Println("defer1的值:", i)
 }()
 return i
}

输出:

defer1的值: 1

defer2的值: 2

main: 2

详解:由于返回值提前声明了,所以在return的时候决定的返回值还是0,但是后面两个defer执行后进行了两次++,将i的值变为2,待defer执行完后,函数将i值进行了返回。

3.defer定义和执行两个步骤,做的事情

先说结论:会先将defer后函数的参数部分的值(或者地址)给先下来【你可以理解为()里头的会先确定】,后面函数执行完,才会执行defer后函数的{}中的逻辑


func test(i *int) int {
 return *i
}
func main(){
 var i = 1
 // defer定义的时候test(&i)的值就已经定了,是1,后面就不会变了
 defer fmt.Println("i1 ="  , test(&i))
 i++
 // defer定义的时候test(&i)的值就已经定了,是2,后面就不会变了
 defer fmt.Println("i2 ="  , test(&i))
 // defer定义的时候,i就已经确定了是一个指针类型,地址上的值变了,这里跟着变
 defer func(i *int) {
  fmt.Println("i3 ="  , *i)
 }(&i)
 // defer定义的时候i的值就已经定了,是2,后面就不会变了
 defer func(i int) {
  //defer 在定义的时候就定了
  fmt.Println("i4 ="  , i)
 }(i)
 defer func() {
  // 地址,所以后续跟着变
  var c = &i
  fmt.Println("i5 ="  , *c)
 }()
 
 // 执行了 i=11 后才调用,此时i值已是11
 defer func() {
  fmt.Println("i6 ="  , i)
 }()
 i = 11
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。如有错误或未考虑完全的地方,望不吝赐教。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Golang 的defer执行规则说明

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

golang defer的执行顺序是什么

在Go语言中,defer语句用于注册延迟调用,延迟调用会在函数执行完毕后被执行,无论函数是正常返回还是发生异常。当有多个defer语句时,它们的执行顺序是逆序的,即最后注册的defer语句会最先执行,最先注册的defer语句会最后执行。
2023-10-22

Golang 函数 defer 的执行顺序和用途

defer 函数在 go 语言中用于延迟函数调用到函数返回前执行,按后进先出 (lifo) 的顺序调用。其用途包括释放资源、记录日志和恢复异常。后延迟的函数将在先延迟的函数之前调用。Go 语言中 defer 函数的执行顺序和用途defer
Golang 函数 defer 的执行顺序和用途
2024-05-24

[教程]JScript中的变量引用规则(补充,函数用法说明) (转)

[教程]JScript中的变量引用规则(补充,函数用法说明) (转)[@more@][教程]jscript中的变量引用规则(补充,函数用法说明)by .NET.com/,http://lostinet.d2g.com">http://www
2023-06-03

linux下定时执行任务的方法及crontab 用法说明(收集整理)

linux下定时执行任务的方法 在LINUX中,周期执行的任务一般由cron这个守护进程来处理[ps -ef|grep cron]。cron读取一个或多个配置文件,这些配置文件中包含了命令行及其调用时间。 cron的配置文件称为“cront
2022-06-04

C#事件标准命名规则及说明(包括用作事件类型的委托命名)

这篇文章主要介绍了C#事件标准命名规则及说明(包括用作事件类型的委托命名),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
2023-02-25

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录