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并不会输出。

参考资料:

http://www.topgoer.com/函数/延迟调用defer.html

https://blog.csdn.net/eclipser1987/article/details/12089271

https://www.cnblogs.com/aiandbigdata/p/10822123.html

https://blog.csdn.net/chr1991/article/details/104771526?utm_medium=distribute.pc_relevant.none-task-blog-title-1&spm=1001.2101.3001.4242

https://www.jianshu.com/p/79c029c0bd58

https://blog.csdn.net/qq_21816375/article/details/78161603

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容