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

Go defer执行时机的深度剖析

2024-03-174.3k 阅读

Go defer 关键字基础

在 Go 语言中,defer 关键字用于延迟函数的执行。当一个函数执行到 defer 语句时,会将 defer 后面的函数调用压入一个栈中,这个栈被称为延迟调用栈。当包含 defer 语句的函数正常返回或者发生恐慌(panic)时,会按照后进先出(LIFO)的顺序依次执行延迟调用栈中的函数。

来看一个简单的示例:

package main

import "fmt"

func main() {
    defer fmt.Println("world")
    fmt.Println("hello")
}

在上述代码中,defer fmt.Println("world") 语句将 fmt.Println("world") 函数调用压入延迟调用栈。当 main 函数执行到 fmt.Println("hello") 后正常返回,此时延迟调用栈中的 fmt.Println("world") 函数被执行,所以输出结果为:

hello
world

defer 在函数返回前执行

defer 语句的函数调用总是在包含它的函数返回之前执行,这包括正常返回和通过 return 语句返回。

package main

import "fmt"

func returnValue() int {
    defer fmt.Println("defer in returnValue")
    return 10
}

func main() {
    result := returnValue()
    fmt.Println("result:", result)
}

returnValue 函数中,defer fmt.Println("defer in returnValue") 会在 return 10 语句执行前被压入延迟调用栈。当 return 10 执行时,函数准备返回,但在真正返回前,会先执行延迟调用栈中的函数,即输出 defer in returnValue,然后函数返回 10main 函数中输出 result: 10

defer 与函数返回值的关系

命名返回值

当函数有命名返回值时,defer 可以修改这个返回值。

package main

import "fmt"

func namedReturnValue() (result int) {
    defer func() {
        result += 5
    }()
    return 10
}

func main() {
    result := namedReturnValue()
    fmt.Println("result:", result)
}

namedReturnValue 函数中,返回值 result 是命名的。defer 语句中的匿名函数在函数返回前执行,它修改了 result 的值,在 return 10 执行后,result 的值变为 15,所以最终输出 result: 15

非命名返回值

对于非命名返回值,defer 不能直接修改返回值。

package main

import "fmt"

func unnamedReturnValue() int {
    var localResult int = 10
    defer func() {
        localResult += 5
    }()
    return localResult
}

func main() {
    result := unnamedReturnValue()
    fmt.Println("result:", result)
}

unnamedReturnValue 函数中,返回值没有命名,localResult 是一个局部变量。defer 语句中的匿名函数在函数返回前执行,它修改了 localResult 的值,但这个修改不会影响到返回值,因为返回值在 return 语句执行时已经确定,所以最终输出 result: 10

defer 在循环中的表现

在循环中使用 defer 时,每次迭代都会将 defer 后的函数调用压入延迟调用栈,并且在循环结束后按照 LIFO 顺序执行。

package main

import "fmt"

func deferInLoop() {
    for i := 0; i < 3; i++ {
        defer fmt.Println("defer in loop:", i)
    }
}

func main() {
    deferInLoop()
}

deferInLoop 函数中,每次循环迭代都会将 fmt.Println("defer in loop:", i) 压入延迟调用栈。循环结束后,延迟调用栈中的函数按照后进先出的顺序执行,输出结果为:

defer in loop: 2
defer in loop: 1
defer in loop: 0

需要注意的是,defer 语句中的 i 是一个引用,在循环结束时 i 的值为 3,但由于每次 defer 压入栈时捕获了当时 i 的值,所以输出的是循环迭代过程中 i 的实际值。

defer 与 panic 和 recover

defer 在 panic 时的执行

当函数发生 panic 时,defer 语句仍然会按照 LIFO 顺序执行。

package main

import "fmt"

func panicWithDefer() {
    defer fmt.Println("defer in panicWithDefer")
    panic("something wrong")
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered from:", r)
        }
    }()
    panicWithDefer()
}

panicWithDefer 函数中,defer fmt.Println("defer in panicWithDefer")panic("something wrong") 之前被压入延迟调用栈。当 panic 发生时,函数不会立即终止,而是先执行延迟调用栈中的函数,输出 defer in panicWithDefer。然后 main 函数中的外层 defer 捕获到 panic,通过 recover 恢复程序并输出 recovered from: something wrong

recover 只能在 defer 中有效

recover 函数只能在 defer 语句中的函数内有效,用于捕获 panic 并恢复程序的正常执行。

package main

import "fmt"

func tryRecover() {
    fmt.Println("before panic")
    panic("test panic")
    fmt.Println("this won't be printed")
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered in defer:", r)
        }
    }()
}

func main() {
    tryRecover()
}

tryRecover 函数中,panic("test panic") 导致程序恐慌。如果 recover 不在 defer 函数内,如在 panic 之后直接调用 recover,是无法捕获到 panic 的。只有在 defer 函数中调用 recover 才能捕获到 panic 并输出 recovered in defer: test panic

defer 与资源管理

defer 常被用于资源管理,例如文件的关闭、数据库连接的关闭等。

文件操作中的 defer

package main

import (
    "fmt"
    "os"
)

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

    // 进行文件读取操作
    // ...
}

func main() {
    readFile()
}

readFile 函数中,defer file.Close() 确保无论文件读取过程中是否发生错误,文件最终都会被关闭。如果没有 defer,在文件读取操作完成后需要手动调用 file.Close(),并且在发生错误提前返回时也需要手动关闭文件,使用 defer 大大简化了资源管理的代码。

数据库连接管理中的 defer

假设使用 database/sql 包连接数据库:

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
)

func queryDatabase() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/testdb")
    if err != nil {
        fmt.Println("connect to database error:", err)
        return
    }
    defer db.Close()

    // 执行数据库查询操作
    rows, err := db.Query("SELECT * FROM users")
    if err != nil {
        fmt.Println("query error:", err)
        return
    }
    defer rows.Close()

    // 处理查询结果
    for rows.Next() {
        var id int
        var name string
        err := rows.Scan(&id, &name)
        if err != nil {
            fmt.Println("scan error:", err)
            return
        }
        fmt.Printf("id: %d, name: %s\n", id, name)
    }
}

func main() {
    queryDatabase()
}

queryDatabase 函数中,defer db.Close() 确保数据库连接在函数结束时关闭,defer rows.Close() 确保查询结果集在使用完毕后关闭,有效地管理了数据库资源。

defer 的性能考量

虽然 defer 非常方便,但在性能敏感的代码中,过多使用 defer 可能会带来一定的性能开销。每次执行 defer 语句时,需要将函数调用压入延迟调用栈,并且在函数返回时需要从栈中弹出并执行这些函数。

例如,在一个高并发且频繁执行的函数中,如果每个函数都使用多个 defer 语句,可能会导致额外的栈操作开销。

package main

import (
    "fmt"
    "time"
)

func performanceTest() {
    start := time.Now()
    for i := 0; i < 1000000; i++ {
        defer fmt.Println("defer in performanceTest:", i)
    }
    elapsed := time.Since(start)
    fmt.Println("time elapsed:", elapsed)
}

func main() {
    performanceTest()
}

上述代码在一个循环中使用大量 defer 语句,运行后可以观察到由于 defer 带来的栈操作开销,程序执行时间会明显增加。在性能关键的代码中,需要谨慎使用 defer,可以考虑在函数结束前手动调用资源释放等操作,以减少 defer 带来的性能损耗。

defer 与闭包的结合使用

闭包在 defer 中的作用

defer 常与闭包结合使用,闭包可以捕获 defer 语句所在作用域的变量。

package main

import "fmt"

func deferWithClosure() {
    var num int = 10
    defer func() {
        fmt.Println("closure in defer, num:", num)
    }()
    num = 20
}

func main() {
    deferWithClosure()
}

deferWithClosure 函数中,defer 后的匿名函数是一个闭包,它捕获了 num 变量。虽然在 defer 语句之后 num 的值被修改为 20,但闭包捕获的是 defer 语句执行时 num 的值,所以输出 closure in defer, num: 10

利用闭包在 defer 中实现复杂逻辑

package main

import "fmt"

func complexDeferLogic() {
    var a, b int = 5, 10
    defer func(x, y int) {
        result := x + y
        fmt.Println("complex defer result:", result)
    }(a, b)
    a = 15
    b = 20
}

func main() {
    complexDeferLogic()
}

complexDeferLogic 函数中,defer 后的闭包接受两个参数 xy,并在闭包内部执行了加法运算。在 defer 语句执行时,ab 的值被传递给闭包,即使之后 ab 的值被修改,闭包内使用的仍然是传递进来的初始值,输出 complex defer result: 15

defer 的嵌套使用

简单的嵌套 defer

package main

import "fmt"

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

func main() {
    nestedDefer()
}

nestedDefer 函数中,有一个外层 defer 和一个内层 defer。内层 defer 所在的代码块结束时,内层 defer 函数调用被压入延迟调用栈,当外层函数结束时,外层 defer 函数调用也被压入延迟调用栈。最终按照 LIFO 顺序执行,输出:

inner defer
outer defer

嵌套 defer 与复杂逻辑

package main

import "fmt"

func complexNestedDefer() {
    defer func() {
        fmt.Println("outer defer 1")
    }()
    {
        defer func() {
            fmt.Println("inner defer 1")
        }()
        defer func() {
            fmt.Println("inner defer 2")
        }()
    }
    defer func() {
        fmt.Println("outer defer 2")
    }()
}

func main() {
    complexNestedDefer()
}

complexNestedDefer 函数中,有多层嵌套的 defer。首先内层的 inner defer 2inner defer 1 按照顺序压入延迟调用栈,然后外层的 outer defer 2outer defer 1 压入延迟调用栈。最终执行顺序为:

inner defer 2
inner defer 1
outer defer 2
outer defer 1

defer 在并发编程中的应用

使用 defer 确保 goroutine 资源释放

package main

import (
    "fmt"
    "sync"
)

func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("worker started")
    // 模拟工作
    time.Sleep(1 * time.Second)
    fmt.Println("worker finished")
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go worker(&wg)
    wg.Wait()
}

worker 函数中,defer wg.Done() 确保无论 worker 函数是正常结束还是发生错误,都会调用 wg.Done() 通知 sync.WaitGroup 任务已完成,避免了在 worker 函数的多个返回点手动调用 wg.Done() 的繁琐。

defer 在并发安全资源管理中的应用

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

type SafeCounter struct {
    value int64
}

func (c *SafeCounter) Increment() {
    atomic.AddInt64(&c.value, 1)
}

func (c *SafeCounter) Get() int64 {
    return atomic.LoadInt64(&c.value)
}

func concurrentDefer() {
    var wg sync.WaitGroup
    counter := SafeCounter{}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }
    wg.Wait()
    fmt.Println("counter value:", counter.Get())
}

func main() {
    concurrentDefer()
}

concurrentDefer 函数中,defer wg.Done() 用于在并发的 goroutine 结束时通知 sync.WaitGroup,同时 SafeCounter 结构体通过原子操作实现并发安全,defer 在并发编程中确保了资源的正确管理和同步。

总结

defer 是 Go 语言中一个强大且实用的特性,它在资源管理、错误处理、函数返回值处理等方面都发挥着重要作用。理解 defer 的执行时机和原理,对于编写高效、健壮的 Go 代码至关重要。在使用 defer 时,需要注意其与函数返回值、循环、panicrecover、并发编程等场景的交互,同时也要考虑性能因素,避免在性能敏感的代码中过度使用 defer。通过合理运用 defer,可以使代码更加简洁、可读且易于维护。