关于go:面试Go-被defer的几个盲区坑了

44次阅读

共计 3852 个字符,预计需要花费 10 分钟才能阅读完成。

大家好,我是二条,是一位从事后端开发的程序员。

上一篇,咱们讲到了 Go 中的字符串为什么不能被批改,这一篇来总结 defer 语句中的几个暗藏的细节。

对于 Go 中的 defer,是做什么的?执行程序是怎么样的?置信学过 Go 语言的同学,曾经不在生疏,明天就来讲讲其中须要把握的几个知识点。

要讲到这几个知识点,还是大抵总结一下 defer 这个内置 关键字

1、defer 是一种提早解决机制,是在函数进行 return 之前进行执行。

2、defer 是采纳栈的形式执行,也就是说先定义的 defer 后执行,后定义的 defer 最先被执行。

正因为 defer 具备这种机制,能够用在函数返回之前,敞开一些资源。例如在某些操作中,连贯了 MySQL、Redis 这样的服务,在函数返回之前,就能够应用 defer 语句对连贯进行敞开。就相似 oop 语言中的 finally 操作一样,不论产生任何异样,最终都会被执行。

其语法格局也十分的简略。

package main
import "fmt"

func main() {function1()
}

func function1() {fmt.Printf("1")
    defer function2()
    fmt.Printf("2")
}

func function2() {fmt.Printf("3")
}

上述代码执行的后果是:

2
1
3

上面就来总结这六个小知识点:

1、defer 的执行程序。采纳栈的形式执行,先定义后执行。

2、defer 与 return 谁先谁后。return 之后的语句先执行,defer 后的语句后执行。

3、函数的返回值初始化与 defer 间接影响。defer 中批改了返回值,理论返回的值是依照 defer 批改后的值进行返回。

4、defer 遇见 panic。依照 defer 的栈程序,输入 panic 触发之前定义好的 defer。

5、defer 中蕴含 panic。依照 defer 的栈程序,输入 panic 触发之前的 defer。并且 defer 中会接管到 panic 信息。

6、defer 下的函数参数蕴含子函数。会先进行子函数的后果值,而后在依照栈的程序进行输入。

defer 的执行程序是什么样的

对于这个问题,后面的示例代码也提到过了,采纳栈的程序执行。在定义时,压入栈中,执行是从栈中获取。

defer 与 return 谁先谁后

先来看如下一段代码,最终的执行后果是怎么样的。

func main() {fmt.Println(demo2())
}

func demo2() int {defer func() {fmt.Println("2")
    }()
    return func() int {fmt.Println("1")
        return 4
    }()}

运行上述代码,失去的后果是:

1
2
4

可能你会有一个疑难❓,既然都提到了 defer 是在函数返回之前执行,为什么还是先输入 1,而后在输入 2 呢?对于 defer 的定义,就是在 函数返回之前执行 。这一点毋庸置疑,必定是在 return 之前执行。 须要留神的是,return 是非原子性的,须要两步,执行前首先要失去返回值 (为返回值赋值),return 将返回值返回调用处。defer 和 return 的执行程序是先为返回值赋值,而后执行 defer,而后 return 到函数调用处。

函数的返回值初始化与 defer 间接影响

同样的形式,咱们先看一段代码,猜想一下最终的执行后果是什么。

func main() {fmt.Println(demo3())
}

func demo3() (a int) {defer func() {a = 3}()
    return 1
}

上诉代码,最终的运行后果如下:

3

跟上第 2 个知识点相似,函数在 return 之前,会进行返回值赋值,而后在执行 defer 语句,最终在返回后果值。

1、在定义函数 demo3()时,为函数设置了一个 int 类型的变量 a,此时 int 类型初始化值默认是 0。

2、定义一个 defer 语句,在函数 return 之前执行,匿名函数中对返回变量 a 进行了一次赋值,设置 a=3。

3、此时执行 return 语句,因为 return 语句是执行两步操作,先为返回变量 a 执行一次赋值操作,将 a 设置为 3。紧接着执行 defer 语句,此时 defer 又将 a 设置为 3。

4、最终 return 进行返回,因为第 3 步的 defer 对 a 进行了从新赋值。因而 a 就变成了 3。

5、最初 main 函数打印后果,打印的其实是 defer 批改之后的值。

如果将变量 a 的申明放回到函数外部申明呢,其运行的后果会依据 return 的值进行返回。

func main() {fmt.Println(demo7())
}

func demo7() int {
    var a int
    defer func(a int) {a = 10}(a)

    return 2
}

上述的最终后果返回值如下:

10
2

为什么会产生两种不同的后果呢?这是因为,这是因为产生了值拷贝景象。在执行 defer 语句时,将参数 a 传递给匿名函数时进行了一个值拷贝的过程。因为值拷贝是不会影响原值,因而匿名函数对变量 a 进行了批改,不会影响函数内部的值。当然传递一个指针的话,后果就不一样了。在函数定义时,申明的变量能够了解为一个全局变量,因而 defer 或者 return 对变量 a 进行了批改,都会影响到该变量上。

defer 遇见 panic。

panic 是 Go 语言中的一种异常现象,它会中断程序的执行,并抛出具体的异样信息。既然会中断程序的执行,如果一段代码中产生了 panic,最终还会调用 defer 语句吗?

func main() {demo4()
}

func demo4() {defer func() {fmt.Println("1")
    }()
    defer func() {fmt.Println("2")
    }()
    panic("panic")
    defer func() {fmt.Println("3")
    }()
    defer func() {fmt.Println("4")
    }()}

运行上述代码,最终失去的后果如下:

╰─ go run defer.go
2
1
panic: panic

goroutine 1 [running]:
main.demo4()

从下面的后果不难看出,尽管产生了 panic 异样信息,还是输入了 defer 语句中的信息,这阐明 panic 的产生,还是会执行 defer 操作。那为什么前面的两个 defer 没有被执行呢。这是因为 pani 的产生,会中断程序的执行,因而后续的代码基本没有拿到执行权。

当函数中产生了 panic 异样,会马上停止以后函数的执行,panic 之前定义的 defer 都会被执行,所有的 defer 语句都会保障执行并把控制权交还给接管到 panic 的函数调用者。这样向上冒泡直到最顶层,并执行(每层的)defer,在栈顶处程序解体,并在命令行中用传给 panic 的值报告谬误状况:这个终止过程就是 panicking。

defer 中蕴含 panic

上一个知识点提到了,程序中尽管产生了 panic,然而在 panic 之前定义的 defer 语句,还是会被执行。要想在 defer 中获取到具体的 panic 信息,须要应用 recover() 进行获取。

func main() {demo5()
}

func demo5() {defer func() {fmt.Println("1")
        if err := recover(); err != nil {fmt.Println(err)
        }
    }()
    defer func() { fmt.Println("2") }()
    panic("panic")
    defer func() { fmt.Println("defer: panic 之后, 永远执行不到") }()}

上述代码执行的后果如下:

2
1
panic

这个(recover)内建函数被用于从 panic 或 谬误场景中复原:让程序能够从 panicking 从新取得控制权,进行终止过程进而恢复正常执行。

defer 下的函数参数蕴含子函数

对于这种场景,可能大家很少遇见,也不是很分明理论的调用逻辑。先来看一段代码。

func main() {demo6()
}

func function(index int, value int) int {fmt.Println(index)
    return index
}

func demo6() {defer function(1, function(3, 0))
    defer function(2, function(4, 0))
}

上诉代码最终执行的后果是:

3
4
2
1

其执行的逻辑是:

1、执行第 1 个 defer 时,压入 defer 栈中,该 defer 会执行一个 function 的函数,在函数返回之前执行。

2、因为该函数中又蕴含了一个函数(子函数),Go 语言解决的机制是,先执行该子函数。

3、执行完子函数,接着再执行第 2 个 defer 语句。此时,第 2 个 defer 中也有一个子函数,依照第 2 点的逻辑,这个子函数会被间接执行。

4、定义完 defer 语句之后,此时完结该函数的调用。所有被定义的 defer 语句,依照栈程序进行输入。

因而能够得出的论断是,当 defer 中存在子函数时,子函数会依照 defer 定义的语句程序,优先执行。defer 最外层的逻辑,则依照栈的程序执行。

总结

对于 defer 的应用,是非常简单的。这里须要留神几点。

1、defer 是在函数返回之前执行,defer 的执行程序是优先于 return。return 的执行是一个两步操作,先对 return 返回的值进行赋值,而后执行 defer 语句,最初将后果进行返回给函数的调用者。

2、即便函数内产生了 panic 异样,panic 之前定义的 defer 依然会被执行。

3、defer 中存在子函数,子函数会依照 defer 的定于程序执行。

正文完
 0