目录
#1 - defer nil函数
#2 - 在循环中使用defer
#3 - 延迟调用含有闭包的函数
#4 - 在执行快中使用defer
#5 - 延迟方法的坑
#6 - defer的执行顺序
#7 - 作用域屏蔽了参数
#8 - 参数很快得到了值
#9 - 循环中存址
#10 - 不返回的意义
如果一个延迟函数被赋值为nil, 运行时的panic异常会发生在外围函数执行结束后而不是defer的函数被调用的时候。例:
func main() {
var run func() = nil
defer run()
fmt.Println("runs")
}
输出结果
runs
❗️ panic: runtime error: invalid memory address or nil pointer dereference
发生了什么?
名为 func 的函数一直运行至结束,然后defer函数会被执行且会因为值为nil而产生panic异常。然而值得注意的是,run()的声明是没有问题,因为在外围函数运行完成后它才会被调用。
上面只是一个简单的案例,但同样的案例也可能发生在真实世界中,所以如果你遇上的话,可以想想是不是掉进了这个坑里。
切忌在循环中使用defer,除非你清楚自己在做什么,因为它们的执行结果常常会出人意料。
虽然,在某些情况下,在循环中使用defer会相当方便,例如将函数中的递归转交给defer,但这显然已经不是本文应该讲解的内容。
func main() {
for {
row, err := db.Query("SELECT ...")
if err != nil {
...
}
defer row.Close()
}
}
在上面的例子中,defer row.Close()在循环中的延迟函数会在函数结束过后运行,而不是每次 for 循环结束之后。这些延迟函数会不停地堆积到延迟调用栈中,最终可能会导致一些不可预知的问题。
解决方案1:不使用defer,直接在末尾调用。
func main() {
for {
row, err := db.Query("SELECT ...")
if err != nil {
...
}
row.Close()
}
}
解决方案2:将任务转交给另一个函数然后在里面使用defer,在下面这种情况下,延迟函数会在每次匿名函数执行结束后执行。
func main() {
for {
func() {
row, err := db.Query("SELECT ...")
if err != nil {
...
}
defer row.Close()
}()
}
}
有时出于某种缘由,你想要让那些闭包延迟执行。例如,连接数据库,然后在查询语句执行过后中断与数据库的连接。例如:
type database struct{}
func (db *database) connect() (disconnect func()) {
fmt.Println("connect")
return func() {
fmt.Println("disconnect")
}
}
运行一下:
db := &database{}
defer db.connect()
fmt.Println("query db...")
输出结果:
query db...
connect
最终的情况是connect()执行结束后,其执行域得以被保存起来,但内部的闭包不会被执行。
解决方案1:
func() {
db := &database{}
close := db.connect()
defer close()
fmt.Println("query db...")
}
(糟糕的)解决方案2:
func() {
db := &database{}
defer db.connect()()
..
}
你可能想要在执行块执行结束后执行在块内延迟调用的函数,但事实并非如此,它们只会在块所属的函数执行结束后才被执行,这种情况适用于所有的代码块除了上文的函数块例如,for,switch 等。
因为:延迟是相对于一个函数而非一个代码块
例如:
func main() {
{
defer func() {
fmt.Println("block: defer runs")
}()
fmt.Println("block: ends")
}
fmt.Println("main: ends")
}
输出结果:
block: ends
main: ends
block: defer runs
上例的延迟函数只会在函数执行结束后运行,而不是紧接着它所在的块(花括号内包含 defer 调用的区域)后执行,就像代码中的演示的那样,你可以使用花括号创造单独的执行块。
如果你希望在另一个块中使用defer,可以使用匿名函数(正如在第二个坑中我们采用的解决方案):
func main() {
func() {
defer func() {
fmt.Println("func: defer runs")
}()
fmt.Println("func: ends")
}()
fmt.Println("main: ends")
}
同样,你也可以使用defer来延迟方法调用,但也可能出一些岔子。例:
type Car struct {
model string
}
func (c Car) PrintModel() {
fmt.Println(c.model)
}
func main() {
c := Car{model: "DeLorean DMC-12"}
defer c.PrintModel()
c.model = "Chevrolet Impala"
}
输出结果:
DeLorean DMC-12
如果使用指针对象作为接收者:
func (c *Car) PrintModel() {
fmt.Println(c.model)
}
则输出结果为:
Chevrolet Impala
例:
func main() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
输出:
3
2
1
0
事实上这是一个作用域的坑,但我想要让你知道它与 defer 和已命名的返回值之间的关系。例:
func release(r io.Closer)(err error) {
defer func() {
if err := r.Close(); err != nil {
...
}
}()
...
return
}
接着创建一个 reader 类型的结构体使得调用 Close 的时候返回一个 error:
type reader struct{}
func (r reader) Close() error {
return errors.New("Close Error")
}
当reader调用Close()的时候总会返回一个 error , release会在defer内部调用。
r := reader{}
err := release(r)
fmt.Print(err)
输出:
nil
延迟函数内的赋值语句在延迟函数的if块中,因此在块中的err变量赋值会创建一个全新的变量,块级变量err的作用域会屏蔽返回变量err,因此,release()还是返回err的原始值。
解决方案:
func release(r io.Closer) (err error) {
defer func() {
if err = r.close(); err != nil {
...
}
}()
...
}
当一个 defer 函数出现而不是被执行的时候,传递给它的参数的值就会被立刻确定下来,例如:
type message struct {
content string
}
func (p *message) set(c string) {
p.content = c
}
func (p *message) print() string {
return p.content
}
试着运行一下上面的代码
func main() {
m := &message{content: "Hello"}
defer fmt.Print(m.print())
m.set("World")
}
输出为"Hello";
在defer中,fmt.Print被推迟到函数返回后执行,可是m.print()的值在当时就已经被求出,因此,m.print()会返回 "Hello",这个值会保存一直到外围函数返回。
当延迟函数执行的时候,会查看当时周围变量中的值 —— 除了被传入参数的值。例如:
for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i)
}()
}
输出:
3
3
3
当代码执行的时候,被延迟的函数会查看当时i的值,这是因为,当defer出现的时候,Go的运行时会保存i的地址,在for循环结束之后i的值变成了3。 因此,当延迟语句运行的时候,所有的延迟栈中的语句都会去查看i地址中的值,也就是3(被当做循环结束后的当时值)。
解决方案1:直接向延迟函数传参
for i := 0; i < 3; i++ {
defer func(i int) {
fmt.Println(i)
}(i)
}
解决方案2:使用一个新的i块级变量来屏蔽循环中的i
for i := 0; i < 3; i++ {
i := i
defer func() {
fmt.Println(i)
}()
}
解决方案3:如果一次延迟调用一个函数,你可以直接对这个函数使用 defer
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
对于调用者来说,在延迟函数中返回值几乎没有什么影响,可是,你依然可以使用命名返回值来影响返回的结果。
例如:
func release() error {
defer func() error {
return errors.New("error")
}()
return nil
}
//返回nil
func release() (err error) {
defer func() {
err = errors.New("error")
}()
return nil
}
//返回"error"