轻松掌握golang的defer机制

什么是defer?

如果熟悉python的话,会感觉defer在某种程度上有点类似于python中的上下文管理,golang中的defer是golang提供的一种延迟调用的机制,可以让一个函数在当前函数执行完毕(即使出错)后执行。

因此显然defer对于那些io流操作很有用,因为io流操作结束之后是需要close的,而程序员很容易忘记,所以defer是个好东西,经常用在资源清理、文件关闭、锁释放等等。

defer的用法

直接把一个函数调用放在defer后面即可。

f, err := os.Open(filepath)
if err != nil {
    panic(err)
}
defer f.Close()
//我们知道f.Close()是会返回一个error的,而defer后面只能是一个函数调用,不能是赋值语句
//不可以是defer _ = f.Close(),那怎么办呢?使用匿名函数即可
//defer func(){_ = f.Close()}()

再比如:

package main

import "fmt"

func main() {
    defer fmt.Println(123)
    fmt.Println(456)
    /*
    456
    123
     */
}

我们看到123是在456之后打印的,defer可以看做是把后面的函数压入了一个栈中,等到当前函数执行完毕,那么在把栈里面的函数依次取出来执行。既然是栈,那么如果有多个defer,执行顺序我想就不需要再多解释了。

package main

import "fmt"

func main() {
    defer fmt.Println(123)
    defer fmt.Println(456)
    defer fmt.Println(789)
    /*
    789
    456
    123
     */
}

另外即使函数在运行时出错了,defer依旧会执行,这样就保证了io句柄的释放。

我们知道如果程序panic了,那么可以使用recover恢复,而recover必须出现在defer语句中。当然不写在defer语句中也可以,但是没有意义。因为只有等程序panic了,执行recover才有意义,所以要将recover写在defer中。

package main

import "fmt"

func main() {
    defer fmt.Println("a")
    //不仅要出现在defer语句中,还必须是在匿名函数里面
    //如果是defer recover()的话,也是不行的
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    defer fmt.Println("b")

    panic("xxx")
    fmt.Println("c")
    /*
    b
    xxx
    a
     */
}

即便出错,依旧会执行defer语句,按照栈的先进后出结构执行,碰到recover进行恢复。另外如果程序panic了,那么即便recover了,panic后面的代码也不会执行了。在执行完所有defer的时候,直接退出了,所以最下面的"c"是不会打印的。另外recover一定要出现在可能引发panic的代码之前,否则也是没用的。

defer的陷阱

来看个例子

package main

import "fmt"

func main() {
    i := 1
    defer fmt.Println("第一个defer:", i)
    defer func() {fmt.Println("第二个defer:", i)}()
    i = 2
    /*
    第二个defer: 2
    第一个defer: 1
     */
}

为什么会出现这种结果呢?其实是这样的,首先defer的后面必须跟一个函数调用,尽管它是先压入栈中,但是参数却是提前确定好了的。比如第一个defer,我们知道golang的函数参数是值传递,那么会把i的值拷贝一份传递给Println,此时参数就已经确定了,于是想打印,这时候defer阻止了它:"老铁,别急,先入栈",但是此时参数就已经确定了。可对于第二个defer来说,我们看到它是在匿名函数里面的,这个匿名函数没有参数,那么不好意思,当匿名函数执行、从而打印的时候,这是的i就是最终的i了,所以是2。第一个defer记得变量i初始的模样,已经印在了脑海里,所以最后即使i变了,第一个defer还是记得它清纯的模样。但是第二个defer就不一样了,对于它来说,它是第一次见到i,所以打印的就是i最终的值。

再来看个复杂点的例子

package main

import "fmt"

func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}

func main() {
    x := 1
    y := 2
    defer calc("AA", x, calc("A", x, y))
    x = 10
    defer calc("BB", x, calc("B", x, y))
    y = 20

    /*
    A 1 2 3
    B 10 2 12
    BB 10 12 22
    AA 1 3 4
    */
}

为什么会是这个结果,我们来分析一下。首先我们知道defer里面的参数是要在入栈之前需要提前确定好的,因此,那么在遇到第一个defer的时候,是不是要执行一下calc("A", x, y)啊,因为它是外层calc函数的参数,所以要在入栈之前先确定好。显然会先打印,A?1?2 3,然后返回了3,那么第一个defer后面要执行的函数就变成了calc("AA", 1, 3),同理第二个defer,会先执行calc("B", x, y) ->?calc("B", 10, 2),从而打印B?10?2?12,返回一个12,那么第二个defer后面要执行的函数就变成了calc("BB", 10, 12)

然后按照defer的顺序,显然会打印BB?10?12?22AA?1?3?4?,所以最终的结果就如代码所示。

defer和return

我们知道如果程序不报错,那么defer会在函数执行完毕之后执行,但是实际上我们说对了一半。什么时候函数才算执行完毕了,我们可以认为当return语句执行完之后这个函数就算执行完毕了。但是defer就是在return语句执行到一半的时候才开始执行的,所以我们算是说对了一半吧。因此golang的return不是原子性的

package main

import "fmt"

func foo() int {
    x := 5
    defer func() {x++}()
    return x
}

func main() {
    fmt.Println(foo()) // 5
}

我们知道golang的返回值是可以起名字的,如果没有起名字,那么你可以认为golang给你的返回值起了一个名字,假设就叫mmp吧,当我们return x的时候,相当于把x的值交给了mmp,正准备返回的时候返现还有defer,于是执行defer,执行完毕之后将返回值返回。即便这个x的值增加了,但是返回的是mmp的值,而这个mmp是5,所以返回的也是5。

package main

import "fmt"

func foo() (x int) {
    defer func() {
        x++
    }()
    return 5
}

func main() {
    fmt.Println(foo()) // 6
}

惊了,我们return 5,居然返回了6。不过仔细看看我们的返回值的定义就能发现问题,我们这里给返回起了一个名字叫x。还记得吗?我们说如果返回值没有名字,那么你可以简单地认为golang给你的返回值取了个名字,当然我们这里定义了名字x,那么return?5等价于,x?= 5;?return?x,但是在return?x之前,x变了,那么return的也是变了之后的x的值,之前说了,golang的return不是原子性的,是会被defer打断的。

package main

import "fmt"

func foo() (y int) {
    x := 5
    defer func() {
        x++
    }()
    return x
}

func main() {
    fmt.Println(foo()) // 5
}

这里我们给返回值起名为y,那么return?x等价于y = x;return?y,但是y?= x执行完之后,会先执行defer,但此时改变的是x的之后,与y无关,所以return?y还是5

package main

import "fmt"

func foo() (x int) {
    defer func(x int) {
        x++
    }(x)
    return 5
}

func main() {
    fmt.Println(foo()) // 5
}

和第二个例子类似,只不过里面接收了参数,但golang是值传递,所以此时的x是一个拷贝,因此值还是原来的5。而且我们发现匿名函数里面的参数x就是误导人的,故意叫x,其实叫y、叫z都是一样的。但是,如果参数是一个指针类型、然后里面是*x++、而我们传递的也是&x的话,那么返回值还是会变成6的。当然这都不重要,我想问一下,如果我在上面的x++下面,打印一下x的话,那么这个x会是多少呢?如果回答6的话,那么要么是你不认真,要么是你还没真正了解defer。仔细分析一下,首先当return?5的时候,才会给x赋值为5,但是初始的话x显然是一个零值,int的话就是0。而x又是defer后面的匿名函数的一个参数,我们说defer虽然入栈、最后执行,但是参数是多少是会先确定的,那么参数x是多少呢?显然就是0啊,那么当x++执行的时候,这个x的值显然是入栈之前就已经确定好的、给参数x传递的值,也就是0,那么++之后,最后就打印1啦。

原文地址:https://www.cnblogs.com/traditional/p/12206996.html

时间: 2024-11-20 03:46:39

轻松掌握golang的defer机制的相关文章

关于golang的defer的练习

golang的defer怎么说.大意就是在函数return后.函数关闭前.按照filo的顺序来执行的关键字 上代码: package main import ( "fmt" ) func main() { // a() fmt.Println(c()) // b() // fmt.Println(d()) } func c() (i int) { defer func() { i++ }() defer fmt.Println("this:", i) return

轻松掌握MySQL数据库锁机制的相关原理

1,SHOW PROCESSLIST查看数据库中表的状态,是否被锁: kill id   //杀掉被锁的表 =================================================== set autocommit=0; select * from t1  where uid='xxxx' for update    //在有索引(例如uid)的情况下是行锁,否则是表锁 insert into t1 values(1,'xxxxx'); commit; ========

golang之defer

概述 对于资源释放,有很多不同的实现方式,不同语言也有不同的惯用方法. C语言 :手动管理 Golang :defer Python :上下文管理器contexManager C++ : 作用域和析构函数 Rust :所有权和drop trait 如果了解上面几种语言的童鞋应该知道, C语言资源管理是比较麻烦的,一旦资源使用过程中出错,就可能造成资源泄漏. Golang通过defer,即使过程中panic,也可以释放资源. Python通过上下文管理器,主要是两个magic function`_

golang中defer的使用规则

在golang当中,defer代码块会在函数调用链表中增加一个函数调用.这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用.因此,defer通常用来释放函数内部变量. 为了更好的学习defer的行为,我们首先来看下面一段代码: func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName)if err != nil {retu

(转)golang 垃圾回收机制

作者:zzZ 原文地址 原文使用 署名 - 非商业性使用 - 相同方式共享 4.0 国际 协议 声明,本文仅仅作为个人mark,排版并不如原文,为了更爽的阅读体验建议到原文查看. 延伸阅读: Go GC: Prioritizing low latency and simplicity 用任何带 GC 的语言最后都要直面 GC 问题.在以前学习 C# 的时候就被迫读了一大堆 .NET Garbage Collection 的文档.最近也学习了一番 golang 的垃圾回收机制,在这里记录一下. 常

golang 之 defer(统计函数执行时间)

1 package main 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 func sum(a ...int) int { 9 defer trace("sum")() // note:不要忘记defer语句后的圆括号,否则本该在进入时执行的操作会在退出时执行,而本该在退出时执行的,永远不会执行 10 total := 0 11 for _, val := range a { 12 total += val 13

golang轮询机制select的理解

func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i < 10; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit) } //select的轮询机制 func fibonacci(c chan int, quit chan int) { x, y := 0, 1 for { select { // select轮询

golang错误处理机制:panic与recover

原文地址:http://www.niu12.com/article/14 panic知识点 package main import ( "fmt" "github.com/pkg/errors" ) func main() { outerFunc() fmt.Println(1) } func outerFunc() { innerFunc() } func innerFunc() { panic(errors.New("An intended fatal

golang中defer的正确使用方式(源自深入解析go)

3.4 defer关键字 defer和go一样都是Go语言提供的关键字.defer用于资源的释放,会在函数返回之前进行调用.一般采用如下模式: f,err := os.Open(filename) if err != nil { panic(err) } defer f.Close() 如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用. 不过如果对defer的了解不够深入,使用起来可能会踩到一些坑,尤其是跟带命名的返回参数一起使用时.在讲解defer的实现之前先看