MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Go语言defer执行顺序的特殊情况

2023-12-303.8k 阅读

Go语言defer基础回顾

在深入探讨Go语言defer执行顺序的特殊情况之前,我们先来回顾一下defer的基本概念和常规执行顺序。

defer语句用于预定一个函数调用,这个函数调用会在包含defer语句的函数返回前被执行。其主要用途是简化资源管理,比如关闭文件、数据库连接等操作。

例如,当我们打开一个文件时,通常需要在函数结束时关闭它,以避免资源泄漏。使用defer可以使代码更加简洁和安全:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("test.txt")
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }
    defer file.Close()

    // 这里进行文件读取等操作
    data := make([]byte, 1024)
    n, err := file.Read(data)
    if err != nil {
        fmt.Println("Error reading file:", err)
        return
    }
    fmt.Println("Read", n, "bytes:", string(data[:n]))
}

在上述代码中,defer file.Close()语句确保了无论os.Open之后的代码如何执行(即使发生错误并提前返回),文件都会在函数main返回前被关闭。

从执行顺序上看,Go语言中的defer遵循“后进先出”(LIFO,Last In First Out)的栈结构。也就是说,如果在一个函数中有多个defer语句,它们会按照逆序执行。例如:

package main

import "fmt"

func main() {
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    defer fmt.Println("defer 3")

    fmt.Println("main function")
}

运行上述代码,输出结果为:

main function
defer 3
defer 2
defer 1

可以看到,defer语句按照定义的逆序执行,在main函数的常规代码执行完毕后依次调用。

特殊情况之循环中的defer

  1. 简单循环中的defer 在循环中使用defer时,情况会变得稍微复杂一些。考虑以下代码:
package main

import "fmt"

func main() {
    for i := 0; i < 3; i++ {
        defer fmt.Println(i)
    }
    fmt.Println("Loop finished")
}

运行这段代码,输出结果为:

Loop finished
2
2
2

这里的输出可能会让人感到意外。原因在于,defer语句在定义时并不会立即绑定变量i的值,而是在defer语句实际执行时才去获取i的值。在循环结束后,i的值为3(由于循环条件i < 3,最后一次循环i的值为2,然后i++使得i变为3),所以每个defer语句在执行时打印的都是i最终的值3 - 1,即2。

  1. 解决循环中defer的变量绑定问题 为了让defer语句在定义时就绑定变量的值,我们可以通过引入一个临时变量来实现。修改上述代码如下:
package main

import "fmt"

func main() {
    for i := 0; i < 3; i++ {
        temp := i
        defer fmt.Println(temp)
    }
    fmt.Println("Loop finished")
}

运行修改后的代码,输出结果为:

Loop finished
2
1
0

此时,每个defer语句在定义时就将i的值赋给了临时变量temp,所以在执行defer语句时,打印的是各自绑定的临时变量的值,符合我们预期的“后进先出”顺序。

  1. 多层循环中的defer 当存在多层循环并且每层循环都有defer语句时,情况会更加复杂。例如:
package main

import "fmt"

func main() {
    for i := 0; i < 2; i++ {
        for j := 0; j < 2; j++ {
            defer fmt.Printf("i: %d, j: %d\n", i, j)
        }
    }
    fmt.Println("All loops finished")
}

运行结果为:

All loops finished
i: 1, j: 1
i: 1, j: 0
i: 0, j: 1
i: 0, j: 0

这里的执行顺序依然遵循“后进先出”原则。最内层循环的defer语句先被定义,然后是外层循环。当所有循环结束后,defer语句按照逆序执行,从最内层循环的最后一个defer开始,依次执行到最外层循环的第一个defer

特殊情况之defer与return的执行顺序

  1. return语句的本质 在Go语言中,return语句并不是原子操作。实际上,return语句可以拆分为两个步骤:首先是返回值赋值,然后是函数返回。而defer语句会在返回值赋值之后、函数真正返回之前执行。

例如,考虑以下函数:

package main

import "fmt"

func returnWithDefer() int {
    var result int
    defer func() {
        result++
    }()
    return result
}

在这个函数中,return result首先将result的值设为0(因为resultint类型,默认初始值为0),然后执行defer语句,defer语句将result的值加1,最后函数返回。所以,returnWithDefer函数返回的值是1,而不是0。

  1. 命名返回值与defer 当函数使用命名返回值时,情况会更加清晰。例如:
package main

import "fmt"

func namedReturnWithDefer() (result int) {
    defer func() {
        result++
    }()
    return 0
}

这里result是命名返回值。return 0语句首先将result赋值为0,然后执行defer语句,defer语句将result的值加1,最后函数返回result,所以函数返回值为1。

  1. 匿名返回值与defer 对于匿名返回值,我们来看下面的例子:
package main

import "fmt"

func anonymousReturnWithDefer() int {
    var temp int
    defer func() {
        temp++
    }()
    return temp
}

在这个例子中,return temptemp的值赋给一个临时变量(用于返回),然后执行defer语句,defer语句修改的是temp,而不是用于返回的临时变量。所以函数返回值为0,因为defertemp的修改不会影响到已经赋值的返回值。

特殊情况之defer与panic和recover

  1. panic与defer的关系 当Go语言程序发生panic时,正常的执行流程会被中断。但是,defer语句依然会按照“后进先出”的顺序执行。例如:
package main

import "fmt"

func panicWithDefer() {
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    panic("something went wrong")
    fmt.Println("this line will not be printed")
}

运行上述代码,输出结果为:

defer 2
defer 1
panic: something went wrong

goroutine 1 [running]:
main.panicWithDefer()
        /tmp/sandbox2162192247/prog.go:7 +0x76
main.main()
        /tmp/sandbox2162192247/prog.go:13 +0x20

可以看到,在panic发生后,defer语句依然按照逆序执行,打印出defer 2defer 1,然后程序输出panic信息并终止。

  1. recover与defer recover是一个内置函数,用于捕获panic并恢复程序的正常执行流程。recover只能在defer函数中使用才有效。例如:
package main

import "fmt"

func recoverWithDefer() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    panic("simulated panic")
    fmt.Println("this line will not be printed")
}

运行上述代码,输出结果为:

Recovered from panic: simulated panic

在这个例子中,defer函数中的recover捕获到了panic,并输出了恢复信息,使得程序没有因为panic而终止。

  1. 多层defer与recover 当存在多层defer并且其中一个defer函数使用recover时,情况会变得复杂一些。例如:
package main

import "fmt"

func multiDeferRecover() {
    defer func() {
        fmt.Println("defer 1")
    }()
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in defer 2:", r)
        }
    }()
    defer func() {
        fmt.Println("defer 3")
    }()
    panic("panic occurred")
    fmt.Println("this line will not be printed")
}

运行上述代码,输出结果为:

defer 3
Recovered in defer 2: panic occurred
defer 1

这里,panic发生后,defer语句按照逆序执行。defer 3先执行,然后defer 2中的recover捕获到panic,最后defer 1执行。

特殊情况之defer在嵌套函数中的执行

  1. 简单嵌套函数中的deferdefer语句出现在嵌套函数中时,其执行顺序也遵循“后进先出”原则,并且与外层函数的defer语句相互独立。例如:
package main

import "fmt"

func outerFunction() {
    defer fmt.Println("outer defer")
    func() {
        defer fmt.Println("inner defer")
        fmt.Println("inner function")
    }()
    fmt.Println("outer function")
}

运行上述代码,输出结果为:

inner function
outer function
inner defer
outer defer

可以看到,内层函数中的defer语句在其函数结束时执行,外层函数中的defer语句在外层函数结束时执行,各自遵循“后进先出”原则。

  1. 复杂嵌套函数中的defer 考虑更复杂的嵌套函数结构,例如:
package main

import "fmt"

func complexNested() {
    defer fmt.Println("defer in complexNested")
    func() {
        defer fmt.Println("defer in inner1")
        func() {
            defer fmt.Println("defer in inner2")
            fmt.Println("inner most function")
        }()
        fmt.Println("inner1 function")
    }()
    fmt.Println("complexNested function")
}

运行上述代码,输出结果为:

inner most function
inner1 function
complexNested function
defer in inner2
defer in inner1
defer in complexNested

这里,从最内层函数到最外层函数,每个函数的defer语句都在该函数结束时按照“后进先出”的顺序执行。

  1. 嵌套函数中defer与外部变量 当嵌套函数中的defer语句引用外部函数的变量时,需要注意变量的生命周期和作用域。例如:
package main

import "fmt"

func outerWithVar() {
    var num int = 10
    func() {
        defer func() {
            fmt.Println("defer in inner:", num)
        }()
        num++
    }()
    fmt.Println("outer:", num)
}

运行上述代码,输出结果为:

outer: 11
defer in inner: 11

在这个例子中,内层函数的defer语句引用了外层函数的变量num。由于numdefer语句执行时仍然有效,所以defer语句打印出的是numdefer语句执行前修改后的值11。

特殊情况之defer在不同作用域中的表现

  1. 块级作用域中的defer Go语言允许在块级作用域(如if - else块、switch块等)中使用defer语句。例如:
package main

import "fmt"

func blockScopeDefer() {
    if true {
        defer fmt.Println("defer in if block")
        fmt.Println("inside if block")
    }
    fmt.Println("outside if block")
}

运行上述代码,输出结果为:

inside if block
outside if block
defer in if block

可以看到,defer语句在包含它的块结束时执行,即使块提前返回(例如在if块中使用return语句),defer语句依然会在块返回前执行。

  1. switch块中的deferswitch块中使用defer也遵循相同的原则。例如:
package main

import "fmt"

func switchScopeDefer() {
    num := 2
    switch num {
    case 1:
        defer fmt.Println("defer in case 1")
        fmt.Println("case 1")
    case 2:
        defer fmt.Println("defer in case 2")
        fmt.Println("case 2")
    default:
        defer fmt.Println("defer in default")
        fmt.Println("default")
    }
    fmt.Println("outside switch block")
}

运行上述代码,输出结果为:

case 2
outside switch block
defer in case 2

在这个例子中,switch语句匹配到case 2,执行case 2中的代码,defer语句在switch块结束时执行。

  1. 多层块级作用域中的defer 当存在多层块级作用域时,每个块级作用域中的defer语句在其所在块结束时执行。例如:
package main

import "fmt"

func multiBlockScopeDefer() {
    if true {
        defer fmt.Println("defer in outer if block")
        if true {
            defer fmt.Println("defer in inner if block")
            fmt.Println("inside inner if block")
        }
        fmt.Println("inside outer if block")
    }
    fmt.Println("outside outer if block")
}

运行上述代码,输出结果为:

inside inner if block
inside outer if block
outside outer if block
defer in inner if block
defer in outer if block

这里,内层if块的defer语句在内层if块结束时执行,外层if块的defer语句在外层if块结束时执行,整体遵循“后进先出”原则。

特殊情况之defer与并发编程

  1. defer在goroutine中的执行 当在goroutine中使用defer时,需要注意defer语句只会在该goroutine结束时执行。例如:
package main

import (
    "fmt"
    "time"
)

func goroutineWithDefer() {
    go func() {
        defer fmt.Println("defer in goroutine")
        fmt.Println("goroutine started")
        time.Sleep(2 * time.Second)
        fmt.Println("goroutine finished")
    }()
    fmt.Println("main function")
    time.Sleep(3 * time.Second)
}

运行上述代码,输出结果为:

main function
goroutine started
goroutine finished
defer in goroutine

可以看到,defer语句在goroutine执行完毕(time.Sleep(2 * time.Second)结束后)时执行。

  1. 多个goroutine中的defer 当有多个goroutine并且每个goroutine都有defer语句时,每个goroutine的defer语句在各自的goroutine结束时独立执行。例如:
package main

import (
    "fmt"
    "sync"
    "time"
)

func multipleGoroutinesWithDefer() {
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer func() {
                fmt.Printf("defer in goroutine %d\n", id)
                wg.Done()
            }()
            fmt.Printf("goroutine %d started\n", id)
            time.Sleep(time.Duration(id) * time.Second)
            fmt.Printf("goroutine %d finished\n", id)
        }(i)
    }
    fmt.Println("main function")
    wg.Wait()
}

运行上述代码,输出结果可能如下(由于goroutine执行顺序的不确定性,实际输出顺序可能不同):

main function
goroutine 0 started
goroutine 1 started
goroutine 2 started
goroutine 0 finished
defer in goroutine 0
goroutine 1 finished
defer in goroutine 1
goroutine 2 finished
defer in goroutine 2

在这个例子中,每个goroutine的defer语句在其自身执行完毕后按照“后进先出”原则执行。

  1. defersync.WaitGroup的配合 在并发编程中,defer经常与sync.WaitGroup配合使用,以确保所有goroutine完成后再继续执行主程序。例如上述代码中,在defer语句中调用wg.Done(),表示该goroutine已经完成,sync.WaitGroup会等待所有注册的wg.Done()调用后再继续执行主程序的后续代码。

深入理解defer的实现机制

  1. 栈结构与defer Go语言通过栈结构来管理defer语句。当一个函数中定义defer语句时,相关的函数调用信息会被压入一个defer栈中。当函数返回时,defer栈中的函数会按照“后进先出”的顺序依次弹出并执行。这就解释了为什么多个defer语句会按照定义的逆序执行。

  2. 运行时支持 Go语言的运行时系统对defer的执行提供了底层支持。在函数执行过程中,运行时会记录defer语句的相关信息,包括要调用的函数、参数等。当函数返回条件满足时,运行时会触发defer栈的处理,依次执行栈中的函数。

  3. 优化与性能考虑 虽然defer语句极大地方便了资源管理和代码编写,但过多地使用defer也可能带来性能开销。因为每次定义defer语句时,都会涉及到栈操作和运行时的一些额外处理。在性能敏感的代码中,需要谨慎使用defer,可以考虑在必要时手动管理资源,以减少性能损耗。例如,在一些高频调用的函数中,如果使用defer来关闭资源,可能会因为频繁的栈操作而影响性能。此时,可以在函数结束前手动调用关闭资源的函数,以提高性能。

实际应用中避免defer相关问题的建议

  1. 明确变量绑定 在循环中使用defer时,务必明确变量绑定,通过引入临时变量等方式,确保defer语句在定义时就绑定正确的值,避免出现意外的结果。

  2. 注意return与defer的顺序 理解return语句与defer语句的执行顺序,特别是在使用命名返回值和匿名返回值时的区别。在编写函数时,要根据实际需求合理安排defer语句,以确保返回值符合预期。

  3. 合理使用defer与并发 在并发编程中,要清楚defer在goroutine中的执行特点,确保defer语句在每个goroutine中正确执行。同时,合理使用sync.WaitGroup等工具,协调多个goroutine的执行和defer语句的调用。

  4. 性能优化 在性能敏感的场景下,对defer的使用进行评估。如果defer带来的性能开销较大,可以考虑手动管理资源,以提升程序的整体性能。

通过深入理解Go语言defer执行顺序的各种特殊情况,开发者能够更加准确和高效地使用defer语句,编写出健壮、可靠且性能良好的Go语言程序。无论是在资源管理、错误处理还是并发编程等方面,defer都提供了强大而灵活的功能,只要正确掌握其使用方法,就能充分发挥Go语言的优势。