Go 延迟调用 defer 用法详解

引子

package counter

import (
    "log"
    "sync"
)

type Counter struct {
    mu    *sync.Mutex
    Value int
}

func NewCounter(value int) *Counter {
    return &Counter{
        new(sync.Mutex), 0,
    }
}

func (c *Counter) Increment() {
    c.mu.Lock()
    // defer func
    defer func() {
        c.mu.Unlock()
        log.Printf("mu sync.Mutex Unlocked!")
    }()
    // safe increment Value
    c.Value++
}

概述

defer (延迟调用)是 Go语言中的一个关键字,一般用于释放资源和连接、关闭文件、释放锁等。
和defer类似的有java的finally和C++的析构函数,这些语句一般是一定会执行的(某些特殊情况后文会提到),不过析构函数析构的是对象,而defer后面一般跟函数或方法。

用法详解

1、 多个defer语句,按先进后出的方式执行

package main

import "fmt"

func main() {
    var whatever [5]struct{}
    for i := range whatever {
        defer fmt.Println(i)
    }
}

输出:

4
3
2
1
0

所有的defer语句会放入栈中,在入栈的时候会进行相关的值拷贝(也就是下面的“对应的参数会实时解析”)。

2、defer声明时,对应的参数会实时解析

简单示例:

package main

import "fmt"

func main() {
    i := 1
    fmt.Println("i =", i)
    defer fmt.Print(i)
}

输出:

i = 1
1

defer后面的语句最后才会执行,后面会讲当defer存在时return的执行逻辑。

辨析:defer后面跟无参函数、有参函数和方法

package main

import "fmt"

//无返回值函数
func test(a int) {
    defer fmt.Println("1、a =", a) //  ④ 方法:值传递
    defer func(v int) { fmt.Println("2、a =", v)} (a) // ③ 有参函数:值传递
    defer func() { fmt.Println("3、a =", a)} () // ② 无参函数:函数调用,此时 a 已经是 2 了,故输出 2
    a++ //  ① defer 之前的最后一行代码行
}
func main() {
    test(1)
}

输出:

3、a = 2
2、a = 1
1、a = 1

解释:
① a++变成2之后,3个defer语句以后声明先执行的顺序执行,
② 无参函数中使用的a现在已经是2了,故输出2。
③ 有参函数中的参数 v,会请求参数,直接把参数代入,所以输出1。
④ 方法中的参数a,直接把参数代入,所以输出1。

3、defer 读取函数返回值(return返回机制)

defer、return、返回值三者的执行逻辑是:

  1. return最先执行,return负责将结果写入返回值中;
  2. 接着defer开始执行一些收尾工作;
  3. 最后函数携带当前返回值(可能和最初的返回值不相同)退出。

当defer语句放在return后面时,不会被执行。

如下:

package main

import "fmt"

func f(i int) int{
    return i
    defer fmt.Print("i =", i) // 在 return i 语句之后,不会被执行
    return i+1 // 不会被执行
}

func main() {
    f(1)
}

没有输出,因为 return i 之后函数就已经结束了,不会执行 defer。

(1)无名返回值:

package main

import (
    "fmt"
)

func a(i int) int {

    defer func() {
        i++
        fmt.Println("defer2:", i)
    }() // ③ 执行: i = 2


    defer func() {
        i++
        fmt.Println("defer1:", i)
    }() // ② 后声明,先执行: i = 1

    return i  // ① i = 0, 已经完成了返回值的赋值,但是这个时候先不返回; 先去执行 defer.
}

func main() {
    var a = a(0)
    fmt.Println("a:", a)
}

输出:

defer1: 1
defer2: 2
a: 0

解释说明:

①返回值由变量 i 赋值,相当于 返回值=i=0。
②第二个defer中 i++ , i= 1, 第一个 defer中i++, i = 2,所以最终i的值是2。
③但是返回值已经被赋值了,即使后续修改i也不会影响返回值。所以, 最终函数的返回值 = 0。

(2)有名返回值:

package main

import (
    "fmt"
)

func b() (i int) { // 有名返回值: 此处函数声明, 已经指明了返回值就是 i
    defer func() {
        i++
        fmt.Println("defer2:", i)
    }()
    defer func() {
        i++
        fmt.Println("defer1:", i)
    }()
    return i // 或者直接写成 return
}

func main() {
    fmt.Println("return:", b())
}

输出:

defer1: 1
defer2: 2
return: 2

解释:
这里已经指明了返回值就是i,所以后续对i进行修改都相当于在修改返回值,所以最终函数的返回值是2。

(3)函数返回值为地址

package main

import (
    "fmt"
)

func c() *int {
    var i int
    defer func() {
        i++
        fmt.Println("defer2:", i)
    }()
    defer func() {
        i++
        fmt.Println("defer1:", i)
    }()
    return &i
}

func main() {
    fmt.Println("return:", *(c()))
}

输出:

defer1: 1
defer2: 2
return: 2

解释:

此时的返回值是一个指针(地址),这个指针 =&i,相当于指向变量i所在的地址,两个defer语句都对 i进行了修改,那么返回值指向的地址的内容也发生了改变,所以最终的返回值是2。

再看一个例子:

func f() (r int) {
    defer func(r int) {
          r = r + 5
    }(r)
    return r // 返回值 r
}

最初返回值r的值是1,虽然defer语句中函数的参数名也叫r,但传参的时候是值传递,返回值 r 并没有被修改,最终的返回值仍是1。

4、defer与闭包( ! 容易写出 bug)

package main

import "fmt"

type Test struct {
    name string
}
func (t *Test) pp() {
    fmt.Println(t.name)
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        defer t.pp()
    }
}

输出:

c
c
c

解释:

for 结束时 t.name=“c”,接下来执行的那些defer语句中用到的 t.name 的值均为”c“。

修改代码为:

package main

import "fmt"

type Test struct {
    name string
}
func pp(t Test) {
    fmt.Println(t.name)
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        defer pp(t) // 这个故事告诉我们,尽量使用"局部变量"
    }
}

输出:

c
b
a

解释:

defer语句中的参数会实时解析,所以在碰到defer语句的时候就把此时的 t 代入了。

再次修改代码:

package main

import "fmt"

type Test struct {
    name string
}
func (t *Test) pp() {
    fmt.Println(t.name)
}

func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        tt := t // 这个故事告诉我们,尽量使用"局部变量"
        println(&tt)
        defer tt.pp()
    }
}

输出:

0xc000010200
0xc000010210
0xc000010220
c
b
a

解释:

① :=用来声明并赋值,连续使用2次a:=1就会报错,但是在for循环内,可以看出每次tt:=t时,tt 的地址都不同,说明他们是不同的变量,所以并不会报错。
② 每次都有一个新的变量tt:=t,所以每次在执行defer语句时,对应的tt不是同一个(for循环中实际上生成了3个不同的tt),所以输出的结果也不相同。

5、defer用于关闭文件和互斥锁

文件

func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }

    defer f.close() // finally close the file

    return ReadAll()
}

互斥锁

var mu sync.Mutex
var m = make(map[string]int)
 
func lookup(key string) int {
    mu.Lock()
    defer mu.Unlock() // 延迟调用 Unlock(), finally
    return m[key]
}

6、“解除”对所在函数的依赖

package main

import "fmt"
import "time"

type User struct {
    username string
}

func (this *User) Close() {
    fmt.Println(this.username, "Closed !!!")
}

func main() {
    u1 := &User{"jack"}
    defer u1.Close()
    u2 := &User{"lily"}
    defer u2.Close()
    time.Sleep(10 * time.Second)
    fmt.Println("Done !")


}

输出:

Done !
lily Closed !!!
jack Closed !!!

解释:
defer后面跟无参函数,u1.Close()和u2.Close()要等 sleep和 fmt.Println(“Done !”)之后才可以执行,也就是在函数最终返回之前执行。

修改代码为:

package main

import "fmt"
import "time"

type User struct {
    username string
}

func (this *User) Close() {
    fmt.Println(this.username, "Closed !!!")
}

func f(u *User) {
    defer u.Close()
}

func main() {

    u1 := &User{"jack"}
    f(u1)

    u2 := &User{"lily"}
    func() { defer u2.Close() }()

    time.Sleep(10 * time.Second)

    fmt.Println("Done !")
}

输出:

jack Closed !!!
lily Closed !!!
Done !

这样的使用方式,似乎不太合理,但却有存在的必要性。大多数情况下,可以用于 u1,u2 之类非常消耗内存,或者cpu,其后执行时间过程且没有太多关联的情况。
既保留了defer的功能特性,也满足范围精确控制的条件 (???)

7、defer与panic

(1)在panic语句后面的defer语句不被执行

func panicDefer() {

    panic("panic")

    defer fmt.Println("defer after panic") // 不会执行到

}

输出:

panic: panic
goroutine 1 [running]:
main.panicDefer()
    E:/godemo/testdefer.go:17 +0x39
main.main()
    E:/godemo/testdefer.go:13 +0x20
Process finished with exit code 2

可以看到 defer 语句没有执行。

(2)在panic语句前的defer语句会被执行

func deferPanic() {

    defer fmt.Println("defer before panic")

    panic("panic")
}

输出:

defer before panic
panic: panic
goroutine 1 [running]:
main.deferPanic()
    E:/godemo/testdefer.go:19 +0x95
main.main()
    E:/godemo/testdefer.go:14 +0x20
Process finished with exit code 2

defer 语句输出了内容。
Go中的panic类似其它语言中的抛出异常,panic后面的代码不再执行(panic语句前面的defer语句会被执行)。

8、调用os.Exit时defer不会被执行

func deferExit() {
    defer func() {
        fmt.Println("defer")
    }() // ① defer func

    os.Exit(0) // 调用 os.Exit(), 不会执行 ① defer func

}

当调用os.Exit()方法退出程序时,defer并不会被执行,上面的defer并不会输出。

本文链接:https://www.dzdvip.com/34359.html 版权声明:本文内容均来源于互联网。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 395045033@qq.com,一经查实,本站将立刻删除。
(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022年7月2日 22:55
下一篇 2022年7月2日 23:44

相关推荐

  • win10的6个实用技巧

    Win10发布了这么多年,关于该操作系统的使用技巧,你知道多少?俗话说磨刀不误砍柴工,掌握一定的使用技巧,娱乐办公都事半功倍,何乐而不为呢!今天带来了基础干货,跟大家分享6个win10快捷方式,好不好用你说了算,一起来看看吧!     【Win+Tab】是打开虚拟桌面的快捷键。 众所周知,虚拟桌面在macOS和Linux操作系统是一个标配,但是在windows操作系统一直没有此功能,直到Win10发布后,微软才在操作系统里内置了这项功能。     使用虚拟桌面,可以快速切换桌面以方便应用,就跟手机使用类似,上拉就能弹出后台的应用,轻松来回切换或关闭。 而电脑的每个虚拟桌面可以被看成是一个独立的工作空间。每创建一个虚拟桌面,就像打开了一个新的工作站。在新的空间里面,你能够开启一套完全不同的任务,而不用担心和以前的任务窗口堆叠混杂。     总之,如果你看不惯在任务栏打开的窗口过多、窗口之间重合交叠、排序非常乱的话,那么用【Win+Tab】快捷键打开虚拟桌面,可以帮助你轻松切换浏览器、任务管理软件、聊天软件等运行的窗口。同时,你也可以自己创建虚拟桌面,为窗口归类。     Win10自带的截图工具也非常好用,允许你自由截取屏幕的画面,按【Win+Shift+S】组合快捷键就能将该功能呼出,选哪截哪! 反正,聊天、办公或者是看电影玩游戏,需要截图的时候,按【Windows+shift+S】,拖动鼠标就能一步搞定,然后再到Word、PPT中按下【Ctrl+V】即可把截图粘贴到文档内。 若想临时取消截图,直接按键盘右上角ESC键取消。     Win10有一个专门针对游戏录制的功能,不仅仅可以录制游戏应用,还可以录制其他窗口,按【Win+G】可以快速召唤录屏工具。 不过首次使用前,需要打开【设置】—【游戏】—【游戏栏】进行设置,下拉就能看到键盘快捷方式,大家可以根据自己的使用习惯,进行快捷键设置,下次启动的时候按下快捷键就能快速调动!     虽然大家对投屏功能已经非常熟识了,不过你可能不知道在win10中怎么使用,其实按【win+P】快捷键就能召唤win10投屏功能,这在我们工作尤其是开会上具有非常大的帮助。     接下来介绍【Ctrl+Shift+T】快捷键,它是重复上一步操作的快捷键。 有时候浏览器打开的网页过多,想要关闭其中一个,但是却误把所有网页都关闭了,又或者是…

    2021年8月23日
    32
  • 现金流量表编制与计算方法总结(现金流量表计算公式模板)

    总有人在后台咨询现金流量表的计算公式,今天给大家准备好了,赶紧收藏! 主表的“经营活动产生的现金流量净额” 1、销售商品、提供劳务收到的现金 =利润表中主营业务收入×(1+16%)+利润表中其他业务收入+(应收票据期初余额-应收票据期末余额)+(应收账款期初余额-应收账款期末余额)+(预收账款期末余额-预收账款期初余额)-计提的应收账款坏账准备期末余额 2、收到的税费返还 =(应收补贴款期初余额-应收补贴款期末余额)+补贴收入+所得税本期贷方发生额累计数 3、收到的其他与经营活动有关的现金 =营业外收入相关明细本期贷方发生额+其他业务收入相关明细本期贷方发生额+其他应收款相关明细本期贷方发生额+其他应付款相关明细本期贷方发生额+银行存款利息收入(公式一) 具体操作中,由于是根据两大主表和部分明细账簿编制现金流量表,数据很难精确,该项目留到最后倒挤填列,计算公式是: 收到的其他与经营活动有关的现金(公式二) =补充资料中“经营活动产生的现金流量净额”-{(1+2)-(4+5+6+7) } 公式二倒挤产生的数据,与公式一计算的结果悬殊不会太大。 4、购买商品、接受劳务支付的现金 =〔利润表中主营业务成本+(存货期末余额-存货期初余额)〕×(1+16%)+其他业务支出(剔除税金)+(应付票据期初余额-应付票据期末余额)+(应付账款期初余额-应付账款期末余额)+(预付账款期末余额-预付账款期初余额) 5、支付给职工以及为职工支付的现金 =“应付工资”科目本期借方发生额累计数+“应付福利费”科目本期借方发生额累计数+管理费用中“养老保险金”、“待业保险金”、“住房公积金”、“医疗保险金”+成本及制造费用明细表中的“劳动保护费” 6、支付的各项税费 =“应交税金”各明细账户本期借方发生额累计数+“其他应交款”各明细账户借方数+“管理费用”中“税金”本期借方发生额累计数+“其他业务支出”中有关税金项目 即:实际缴纳的各种税金和附加税,不包括进项税。 7、支付的其他与经营活动有关的现金 =营业外支出(剔除固定资产处置损失)+管理费用(剔除工资、福利费、劳动保险金、待业保险金、住房公积金、养老保险、医疗保险、折旧、坏账准备或坏账损失、列入的各项税金等)+营业费用、成本及制造费用(剔除工资、福利费、劳动保险金、待业保险金、住房公积金、养老保险、医疗保险等)+其他应收款本期借方发生额+其…

    2022年8月23日
    49
  • 荣耀 X10和荣耀 9X的硬件配置参数对比(荣耀x10和荣耀9x的区别在哪里)

    荣耀 9X 荣耀 X10 性能 麒麟810 LPDDR 4X UFS 2.1 麒麟820 5G LPDDR 4X UFS 2.1 屏幕 6.59英寸 60Hz LCD直屏 分辨率 2340*1080 6.63英寸 90Hz LCD直屏 分辨率 2400*1080 充电续航 4000mAh 10W有线充电 4300mAh 22.5W有线充电 相机 后置4800万像素 索尼IMX582 主摄 200万像素景深镜头 前置1600万像素 升降镜头 后置4000万像素 索尼IMX600/RYYB 主摄 800万像素超广角镜头 200万像素微距镜头 前置1600万像素 升降镜头 细节 侧边指纹解锁 塑料边框 玻璃后盖 8.8mm 206g 侧边指纹解锁 塑料边框 玻璃后盖 8.8mm 203g 首发价 4GB+64GB 1399 6GB+64GB 1599 6GB+128GB 1899 6GB+64GB 1899 6GB+128GB 2199 8GB+128GB 2399 荣耀 9X 荣耀 X10 荣耀 9X和荣耀 X10作为荣耀 X系列最经典的两款手机,他们两个的配置在同价位也是十分出众。麒麟810和麒麟820也被称为一代神U。现在还在用这两款手机的用户应该不少吧

    2022年10月18日
    39
  • 消失的夫妻比笔录更真实的细节(消失的夫妻凶手性格分析)

    在2013年,山东费县发生了一起杀人案件,像如此凶残的案件,在全国也鲜有发生。 很多当地人至今回忆起当年的场景,仍然心有余悸。 随后《天网》节目根据该事件改编了一档普法栏目剧《消失的夫妻》,可仅仅是播过一次就下架了。因为案件细节实在太令人揪心。单单只是作为观众,就觉得不寒而栗。 一、 2015年5月15日,山东省费县公安局某派出所接到群众刘大娘的报案称,自己的儿子孙刚、儿媳李红可能在打架后离家出走了。此外,刘大娘还向警方提供了一个线索,那就是儿子和儿媳结婚还不到半年。 一开始,派出所的民警也觉得非常奇怪。好端端的新婚夫妇怎么会无缘无故打架,然后双双离家出走呢?不过,既然接到群众报案,派出所的民警还是立马就开车前往事发地点调查。 原来在5月14日这天晚上,刘大娘的儿子给他打电话说第二天会过来吃早饭。所以到了15日这天,天刚蒙蒙亮,刘大娘就早早就起床做了一桌早饭,只等着儿子、儿媳妇过来吃饭。 费县是山东省临沂市一个普通的小县城,平时基本上没有太多的外来人员。刘大娘平时与丈夫住在一起,而她的儿子和儿媳妇也才结婚不久。只不过,小两口和她没有住在一起,而是住在村里南边的新房里。新房位于村子边上,周边非常偏僻,平时也没有什么人路过。为了安全起见,孙刚在新房外边安有监控摄像头。 可是,刘大娘从早上7点一直等到早上10点半都没有等到儿子儿媳过来。这天早上,刘大娘总是莫名地觉得胸口疼痛。她担心小两口出了什么事,于是走路到儿子家去看看。 新房院子外面的大门朝外开着,等刘大娘走到院子里,却发现儿子和儿媳养的看家护院的狗被人用刀砍死了,连墙上都是狗的血迹。眼前这一幕吓得她赶紧往屋子里跑,想看看小两口怎么了。 进到房间里,刘大娘却看到屋内打扫得很干净,连夫妻俩的床单被罩都是刚刚新铺好的,只不过儿媳妇的红内衣被扯得稀巴烂,扔在地上。刘大娘这才稍微舒了一口气,她以为儿子和儿媳只是负气打架离家出走了,希望请警察把他们找回来好好调解一下。 接到报案后,民警立马来到了现场调查。作为办案经验丰富的民警,任何蛛丝马迹都无法逃脱他们的眼睛。民警到达现场后,第一眼就发现了不对劲的地方,那就是院子外边装的监控器居然被人用剪刀剪断了电线。 这是第一个疑点,夫妻打架出走为什么会剪断监控摄像头的电线呢,这就不合理。 事情绝没这么简单!当时几位民警心里一闪而过这个念头。 果然,民警虽然发现新房里的床单是新铺好的,可是床…

    2022年8月15日
    1.2K
  • 深度解析抖音小店

    抖音小店是抖音为商家提供的电商服务平台,帮助商家在抖音拓宽变现渠道,提升流量价值。 开通抖音小店店铺后,商家可以在PC版商家管理后台进行商品创建和管理、订单查询、发货、售后、结算等基础操作。

    2021年5月16日
    44
  • 王者荣耀s24赛季四款新皮肤曝光

    随着s24赛季的临近,玩家们的心也渐渐长草,为什么这么说呢?每个新赛季都会有很多的期待,已经官宣的我们只需要等就行了,而对于没有曝光,且确定会有的东西,那个好奇心和期待的心理是很强烈的,就比如战令奖励。 以往赛季,排位限定皮肤是玩家期待的,但是随着这么多版本的更新,我们都知道,排位赛季皮肤的期待值并不高,因为几乎都是冷门英雄,加上现在会提前很早就爆料,所以无需等待即可知晓。 s23赛季,米莱狄的战令皮肤可以说收获了很多的好评,创意值满分,特效诚意十足,所以对于新赛季的战令可能会更加期待,有玩家就爆料出了四款皮肤的半身像,所以可以简单猜测下,其中大概率就有战令的两个皮肤。 干将莫邪新皮肤,疑似胡桃系列 这个半身像的形象像爸爸和女儿似的,但大家都知道,干将莫邪本是情侣,所以这是一对欧式的情侣,从画风和服装上来看,非常像米莱狄的胡桃异想国的皮肤,所以如果没出现意外,这本应该是s23赛季的一级勇者皮,但是却被达摩的皮肤插了队。 这次被玩家爆出,那肯定是在正式服或者体验服加入了这文件包,目的只有一个,那就是s24赛季上线,所以新赛季一级战令皮肤奖励,大概率是这个干将莫邪的皮肤。 安琪拉漫画皮,去年已经被爆料 安琪拉的确有一款史诗品质的皮肤,以漫画为主题,甚至连完整的语音包都被玩家解析出来了,皮肤肯定是有的,品质肯定是史诗,而这个时间点重新爆出该皮肤的半身像,那么目的就路人皆知了,s24这款皮肤会上线。 那么非常有概率是安琪拉的这款漫画皮肤,品质对应上,时间对应上,每个赛季初都会上线战令皮肤,但是不一定每个赛季初都有史诗皮肤。 典韦汽车人皮肤,活动获取,或者六元勇者 典韦本身有战令皮肤,有五岳皮肤,不缺史诗,且看这个半身像也不太可能是传说皮肤,那么大概率就是勇者皮,我想也有可能是和吕布的狼爪一样,联动一个汽车品牌,或许像赵云的引擎之心一样,如果上线了,那就是王者荣耀第二款汽车人皮肤了。 考虑品质,大概率是勇者,所以要么活动免费获取,要么就是六元秒杀,我觉得活动免费获得的概率可能会更大一点。 扁鹊早就被曝光的皮肤,大概率六元秒杀,可能免费获取 在去年廉颇的六元皮上线的时候就有玩家发现了,这个皮肤的海报中有一个双色头发的英雄,经过鉴定是扁鹊无疑,这次完整的半身像被曝光,那么这个皮肤估计真的要上了,因为和廉颇秒杀皮一起出现,所以六元秒杀的概率是很大的。 因为体验服扁鹊已经进行了大幅度…

    2021年6月16日
    22