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

Godefer的执行机制

2022-08-253.0k 阅读

Go defer 关键字概述

在Go语言中,defer 是一个非常有用的关键字。它用于预定一个函数调用,这个被预定的函数会在包含 defer 语句的函数返回之前执行。defer 语句后面跟着一个函数调用,这个函数可以是一个普通的函数,也可以是一个匿名函数。它的主要目的是确保在函数结束时,某些操作(如关闭文件、解锁互斥锁等)一定会被执行,而无论函数是正常返回还是因为错误而提前返回。

defer 的语法

defer 的语法非常简单,基本形式如下:

defer functionCall()

这里的 functionCall() 可以是任何合法的函数调用,包括自定义函数、标准库函数以及匿名函数。例如:

package main

import (
    "fmt"
)

func main() {
    defer fmt.Println("This is a deferred function call")
    fmt.Println("This is the main function")
}

在上述代码中,fmt.Println("This is a deferred function call")defer 关键字预定。当 main 函数执行完毕时,这个被预定的函数调用会被执行,所以程序的输出结果为:

This is the main function
This is a deferred function call

defer 的执行机制细节

  1. 延迟执行时机
    • defer 函数调用会在包含 defer 语句的函数即将返回时执行。这意味着无论函数是通过 return 语句正常返回,还是因为 panic 导致异常退出,defer 函数都会被执行,除非程序在 defer 函数执行前就被强制终止(例如调用了 os.Exit 函数)。
    • 来看一个简单的示例,函数中有多个 defer 语句:
package main

import (
    "fmt"
)

func multipleDefers() {
    defer fmt.Println("First defer")
    defer fmt.Println("Second defer")
    fmt.Println("Function body")
}

multipleDefers 函数中,有两个 defer 语句。当函数执行到 fmt.Println("Function body") 后,即将返回时,会按照后进先出(LIFO,Last In First Out)的顺序执行 defer 函数。所以输出结果为:

Function body
Second defer
First defer
  1. 参数求值时机
    • defer 语句中的函数参数会在 defer 语句执行时求值,而不是在函数实际执行时求值。这一点需要特别注意,因为如果参数是变量,并且在 defer 语句执行后变量的值发生了变化,defer 函数中使用的仍然是 defer 语句执行时参数的值。
    • 以下面的代码为例:
package main

import (
    "fmt"
)

func deferParameterEvaluation() {
    i := 1
    defer fmt.Println("Deferred value:", i)
    i = 2
    fmt.Println("Current value:", i)
}

在这个函数中,defer fmt.Println("Deferred value:", i) 语句在执行时,i 的值为 1,尽管之后 i 的值被修改为 2。所以输出结果为:

Current value: 2
Deferred value: 1
  1. 与 return 语句的关系
    • 在Go语言中,return 语句并不是原子操作,它实际上可以分为两个步骤:返回值赋值和函数返回。defer 函数的执行发生在返回值赋值之后,但在函数真正返回之前。
    • 考虑下面的代码:
package main

import (
    "fmt"
)

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

在这个函数中,return result 首先将 result 的值(初始为 0)赋值给返回值,然后执行 defer 函数,defer 函数将 result1,但此时返回值已经确定为 0。所以函数返回值为 0

  • 如果要让 defer 函数影响返回值,可以使用命名返回值。例如:
package main

import (
    "fmt"
)

func deferWithNamedReturn() (result int) {
    defer func() {
        result++
    }()
    return result
}

在这个函数中,返回值被命名为 resultreturn result 时,由于 result 是命名返回值,在 defer 函数执行后,result 的值为 1,所以函数返回值为 1

defer 与异常处理(panic 和 recover)

  1. panic 时 defer 的执行
    • 当函数发生 panic 时,defer 函数同样会被执行。panic 会导致程序沿着调用栈向上抛出异常,在这个过程中,每一层函数中定义的 defer 函数都会被依次执行。
    • 看下面的代码示例:
package main

import (
    "fmt"
)

func innerFunction() {
    defer fmt.Println("Inner function defer")
    panic("Inner function panic")
}

func outerFunction() {
    defer fmt.Println("Outer function defer")
    innerFunction()
}

func main() {
    outerFunction()
}

innerFunction 中发生了 panic,但在 panic 向上传播之前,innerFunction 中的 defer 函数会被执行。接着 panic 传播到 outerFunctionouterFunction 中的 defer 函数也会被执行。所以输出结果为:

Inner function defer
Outer function defer
panic: Inner function panic

goroutine 1 [running]:
main.innerFunction()
        /tmp/sandbox2244322537/prog.go:6 +0x59
main.outerFunction()
        /tmp/sandbox2244322537/prog.go:10 +0x3f
main.main()
        /tmp/sandbox2244322537/prog.go:14 +0x21
  1. recover 与 defer
    • recover 函数用于捕获 panic 异常,使程序可以从 panic 中恢复并继续执行。recover 只有在 defer 函数中调用才有效。
    • 下面是一个使用 recover 的示例:
package main

import (
    "fmt"
)

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

func main() {
    recoverFromPanic()
    fmt.Println("Program continues after recovery")
}

recoverFromPanic 函数中,defer 函数使用 recover 捕获了 panic。如果捕获到 panic,就打印恢复信息。程序输出为:

Recovered from panic: Simulated panic
Program continues after recovery

defer 在资源管理中的应用

  1. 文件操作
    • 在Go语言中,对文件进行操作时,需要在操作完成后关闭文件以释放资源。defer 是实现这一操作的理想选择。
    • 以下是一个读取文件内容的示例:
package main

import (
    "fmt"
    "os"
)

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

    var content []byte
    content, err = os.ReadFile(filePath)
    if err != nil {
        fmt.Println("Error reading file:", err)
        return
    }
    fmt.Println("File content:", string(content))
}

在这个函数中,defer file.Close() 确保无论文件读取是否成功,文件都会在函数结束时关闭。 2. 数据库连接

  • 类似地,在与数据库交互时,也需要在操作完成后关闭数据库连接。
  • 假设使用 database/sql 包连接MySQL数据库,示例代码如下:
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)/test")
    if err != nil {
        fmt.Println("Error opening database:", err)
        return
    }
    defer db.Close()

    rows, err := db.Query("SELECT * FROM users")
    if err != nil {
        fmt.Println("Error querying database:", err)
        return
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        err := rows.Scan(&id, &name)
        if err != nil {
            fmt.Println("Error scanning row:", err)
            return
        }
        fmt.Printf("ID: %d, Name: %s\n", id, name)
    }
    if err = rows.Err(); err != nil {
        fmt.Println("Error iterating rows:", err)
    }
}

在这个示例中,defer db.Close()defer rows.Close() 分别确保数据库连接和查询结果集在函数结束时被正确关闭。

defer 在并发编程中的应用

  1. 互斥锁的使用
    • 在并发编程中,使用互斥锁(sync.Mutex)来保护共享资源是常见的做法。defer 可以方便地确保在函数结束时释放互斥锁。
    • 以下是一个简单的示例:
package main

import (
    "fmt"
    "sync"
)

var mu sync.Mutex
var sharedData int

func updateSharedData() {
    mu.Lock()
    defer mu.Unlock()

    sharedData++
    fmt.Println("Updated shared data:", sharedData)
}

updateSharedData 函数中,defer mu.Unlock() 确保无论函数是正常返回还是因为错误提前返回,互斥锁都会被释放,从而避免死锁。 2. WaitGroup 的使用

  • sync.WaitGroup 用于等待一组 goroutine 完成。defer 可以与 WaitGroup 配合使用,确保在函数结束时 WaitGroup 的计数被正确减少。
  • 示例代码如下:
package main

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

func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Worker started")
    time.Sleep(2 * time.Second)
    fmt.Println("Worker finished")
}

func main() {
    var wg sync.WaitGroup
    wg.Add(3)

    for i := 0; i < 3; i++ {
        go worker(&wg)
    }

    wg.Wait()
    fmt.Println("All workers have finished")
}

worker 函数中,defer wg.Done() 确保在函数结束时 WaitGroup 的计数减少,从而使主函数能够正确等待所有 goroutine 完成。

defer 的性能考量

  1. 开销分析
    • defer 机制在实现上是有一定开销的。每次执行 defer 语句时,会创建一个 defer 记录,这个记录包含了要调用的函数以及函数参数等信息。当函数返回时,需要遍历这些 defer 记录并按顺序执行相应的函数。
    • 虽然这种开销在大多数情况下是可以接受的,但在一些对性能要求极高且频繁调用的函数中,过多使用 defer 可能会对性能产生一定影响。
  2. 优化建议
    • 如果在性能敏感的代码段中使用 defer,可以考虑减少 defer 的使用数量。例如,对于一些简单的资源管理操作,可以手动在合适的位置进行资源释放,而不是依赖 defer
    • 另外,由于 defer 函数参数在 defer 语句执行时求值,如果参数求值的开销较大,并且在 defer 函数执行前参数值不会改变,可以提前计算好参数值,以避免不必要的重复计算。

defer 与闭包的结合使用

  1. 闭包在 defer 中的应用
    • defer 经常与闭包结合使用,以实现更灵活的逻辑。闭包可以捕获其定义时的环境变量,在 defer 函数中使用这些变量可以实现一些复杂的操作。
    • 例如,下面的代码通过闭包在 defer 函数中记录函数执行的时间:
package main

import (
    "fmt"
    "time"
)

func measureExecutionTime() {
    startTime := time.Now()
    defer func() {
        elapsedTime := time.Since(startTime)
        fmt.Printf("Function execution time: %s\n", elapsedTime)
    }()

    // 模拟一些工作
    time.Sleep(3 * time.Second)
}

在这个函数中,闭包捕获了 startTime 变量,在 defer 函数执行时计算函数的执行时间。 2. 闭包与 defer 的注意事项

  • 当在 defer 中使用闭包时,同样要注意变量作用域和值捕获的问题。如果闭包捕获的变量在 defer 语句执行后发生变化,defer 函数中使用的仍然是闭包定义时变量的值。
  • 以下面的代码为例:
package main

import (
    "fmt"
)

func deferClosureIssue() {
    var numbers []int
    for i := 0; i < 3; i++ {
        defer func() {
            fmt.Println(i)
        }()
        numbers = append(numbers, i)
    }
    fmt.Println("Numbers:", numbers)
}

在这个函数中,defer 闭包捕获了 i 变量。由于 defer 语句在循环中执行,当函数返回时,i 的值已经变为 3,所以三个 defer 函数都会打印 3,而不是预期的 012。要解决这个问题,可以将 i 作为闭包的参数传递,这样每个闭包会捕获 i 的不同值:

package main

import (
    "fmt"
)

func deferClosureFixed() {
    var numbers []int
    for i := 0; i < 3; i++ {
        defer func(j int) {
            fmt.Println(j)
        }(i)
        numbers = append(numbers, i)
    }
    fmt.Println("Numbers:", numbers)
}

在修正后的代码中,每个闭包捕获了 i 的不同值,所以 defer 函数会分别打印 210

总结 defer 的执行机制要点

  1. 执行时机defer 函数在包含 defer 语句的函数返回之前执行,遵循后进先出的顺序。
  2. 参数求值defer 函数的参数在 defer 语句执行时求值。
  3. 与 return 的关系defer 函数在返回值赋值之后、函数真正返回之前执行,命名返回值会受到 defer 函数的影响。
  4. 异常处理panicdefer 函数仍会执行,recover 只能在 defer 函数中有效捕获 panic
  5. 资源管理defer 广泛应用于文件、数据库连接等资源的管理,确保资源在函数结束时正确释放。
  6. 并发编程:在并发编程中,defer 用于确保互斥锁释放和 WaitGroup 计数正确减少。
  7. 性能考量defer 有一定开销,在性能敏感代码中需谨慎使用。
  8. 与闭包结合defer 常与闭包结合使用,但要注意变量作用域和值捕获问题。

通过深入理解 defer 的执行机制,开发者可以在Go语言编程中更有效地利用这一特性,编写出更健壮、优雅的代码。无论是资源管理、异常处理还是并发编程,defer 都为Go语言开发者提供了强大的工具。在实际应用中,根据具体的业务需求和性能要求,合理地使用 defer,可以提高代码的可读性和可维护性,同时避免资源泄漏和其他潜在问题。