• 不破楼兰终不还——Go 延迟语句defer指南


    不破楼兰终不还——Go 延迟语句defer指南

    说到defer,很多gopher都知道这是求职面试常考点,也是一个易错的难点,特别是延迟语句defer也是Golang一个十分重要的关键字。所以掌握defer刻不容缓!

    什么是defer?

    现在我们编程经常要操作文件或数据库,而进行数据库和文件操作就会涉及数据库和文件的关闭,用完不关闭就会导致内存泄露,可能会导致很严重的安全问题。但是这也是我们经常忘记的一个步骤,所以defer可以很好的解决这个问题,比如我们连接数据库就使用defer编写关闭语句,这就能较好的帮助开发人员编写更安全的程序。

    不过defer运行时会带来一定时间的开销,因此如果对耗时要求特别严格,建议不使用defer。

    我们来看下面这个例子:

    var l sync.RWMutex
    l.Lock()
    panic("异常信息")
    l.Unlock()
    
    • 1
    • 2
    • 3
    • 4

    如果上面这种情况,使用锁和解锁语句之间出现了panic,就会形成死锁,即使我们不使用panic语句,其他语句也可能导致panic啊,因此我们需要使用defer。

    image-20220827104219450

    defer的执行顺序是什么?

    根据Golang官方文档描述,defer就像一个LIFO的栈,每次执行defer语句,都会将函数”压栈“,函数参数也会被保存下来;如果外层函数(非代码块)退出,最后的defer语句就会执行,也就是栈顶的函数或方法会被执行。

    不过需要注意:

    如果defer执行的语句是一个nil,那么就会在调用时产生panic。

    一般情况下多个defer的执行顺序,我们可以通过下面这个例子了解:

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	defer fmt.Println("defer 1")
    	defer_test()
    	defer fmt.Println("defer 2")
    }
    
    func defer_test() {
    	defer fmt.Println("defer 3")
    	defer fmt.Println("defer 4")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果为:

    defer 4
    defer 3
    defer 2
    defer 1
    
    Program exited.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    defer就像一个LIFO的栈,最后被定义的defer最先执行。

    image-20220827104251903

    参数传递无外乎就是传值(pass by value),传引用(pass by reference)或者说是传指针。在Go语言中,按引用传递其实也可以称作”按值传递”,只不过该副本是一个地址的拷贝,通过它可以修改这个值所指向的地址上的值。

    使用defer时,涉及到函数参数和闭包引用。使用函数参数方式,defer会在定义时取值并保存起来。而使用闭包引用的方式,虽然也是值传递,但是拷贝的是函数指针。

    举个栗子:

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	var array [5]int = [5]int{5, 4, 3, 2, 1}
    	for _, i := range array {
    		defer func() { fmt.Println(i) }()
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行结果如下:

    1
    1
    1
    1
    1
    
    Program exited.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    defer语句全是输出1,因为循环结束后i=1,而使用匿名函数让defer后面跟着的是一个“闭包”,所以i是“引用类型”的变量。

    如果对上面这段代码稍作修改,得到的结果就不一样了:

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	var array [5]int = [5]int{5, 4, 3, 2, 1}
    	for _, i := range array {
    		defer fmt.Println(i)
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果如下:

    1
    2
    3
    4
    5
    
    Program exited.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    调用 defer 关键字会立刻拷贝函数中引用的外部参数,因此当i从5到1时,所有的值都被拷贝下来。

    image-20220827104030980

    defer与return的执行顺序

    defer用得好则已,用得不好就会带来灾难。

    能不能用好就得看我们能不能理解retrun语句。

    一条return语句,其实不是一条原子指令,其大概可以分为三条指令:

    • 返回值为xxx
    • 调用defer函数
    • 空的return

    举个栗子:

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	fmt.Println("return:", banana()) 
    }
    
    func banana() (i int) {
    	defer func() {
    		i++
    		fmt.Println("defer 2:", i) 
    	}()
    	defer func() {
    		i++
    		fmt.Println("defer 1:", i) 
    	}()
    	return i 
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    defer 1: 1
    defer 2: 2
    return: 2
    
    Program exited.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这是有名返回值的情况,接下我们来看一看匿名返回值的情况:

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	fmt.Println("return:", apple())
    }
    
    func apple() int {
    	var i int
    	defer func() {
    		i++
    		fmt.Println("defer 2:", i)
    	}()
    	defer func() {
    		i++
    		fmt.Println("defer 1:", i)
    	}()
    	return i
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    defer 1: 1
    defer 2: 2
    return: 0
    
    Program exited.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面这两段代码说明了:defer语句只能访问有名返回值,不能直接访问匿名返回值。

    但是如果是下面这种情况:

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	fmt.Println("return:", banana())
    }
    
    func banana() (i int) {
    	defer func(i int) {
    		i++
    		fmt.Println("defer 2:", i)
    	}(i)
    	defer func(i int) {
    		i++
    		fmt.Println("defer 1:", i)
    	}(i)
    	return i
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    输出结果就为:

    defer 1: 1
    defer 2: 1
    return: 0
    
    Program exited.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这是因为传递给defer后面的匿名函数的是形参的一个复制值,不会影响实参i。

    参考文献

    机械工业出版社 《Go程序员面试笔试宝典》

    defer的执行顺序与时机 https://studygolang.com/articles/22931

    理解 Go 语言 defer 关键字的原理 | Go 语言设计与实现

    https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-defer/#531-现象

    GO函数传参 []int 与 [3]int 有何区别?https://segmentfault.com/q/1010000020543158?bd_source_light=4746641

    Golang中defer、return、返回值之间执行顺序的坑 https://cloud.tencent.com/developer/article/1410243

  • 相关阅读:
    骨传导原理是什么?哪些骨传导耳机值得入手
    @postconstruct注解获取不到本身的bean
    java.lang.Float类下intValue()方法具有什么功能呢?
    基于AVDTP信令分析蓝牙音频启动流程
    Java原生执行Shell 文件
    初步读懂linux内核结构
    反射和注解
    【Pinia和Vuex区别】
    【vue设计与实现】组件的实现原理 4 - setup函数的作用与实现 & 组件事件与emit的实现
    MySQL----(五)索引、视图以及设计三范式
  • 原文地址:https://blog.csdn.net/qq_36045898/article/details/126555392