Golang defer 关键字
程序在运行时常常会创建一些临时资源,比如文件句柄或网络连接。这些资源在使用完毕后需要被清理掉,以确保不会造成资源泄漏或其他问题。清理工作必须进行,不管函数有多少种退出方式(比如正常返回、错误返回、抛出异常等),也不管函数是否成功执行完毕。这意味着无论函数执行路径如何,资源清理是必须执行的。在 Go 语言中,可以使用 defer 关键字来安排资源清理代码。当在函数中使用 defer 语句时,实际上是在指示 Go Runtime 在当前函数执行完毕准备返回结果之前,去执行 defer 后面指定的操作。这确保了无论函数以何种方式退出,defer 后面的代码都会被执行,从而保证了资源的正确清理。
接下来,通过编写一个简化版的 cat 程序⁶来演示 defer 关键字的使用。
func main() {
if len(os.Args) < 2 { // ❶ 检查命令参数数量是否小于2(程序本身算一个参数)
log.Fatal("no file specified") // ❷ 若未指定文件名,打印错误并退出程序
}
f, err := os.Open(os.Args[1]) // ❸ 尝试打开命令行中指定的第一个文件
if err != nil {
log.Fatal(err)
}
// ❹ 使用 defer 语句确保在 main 函数返回前关闭文件,以防文件句柄泄漏
defer f.Close()
data := make([]byte, 2048) // 创建字节切片,用于存储从文件中读取的数据
for {
count, err := f.Read(data) // ❺ 从文件中读取数据到 data 切片中
os.Stdout.Write(data[:count]) // 将读取的数据写入标准输出(通常是终端)
if err != nil { // ❻ 检查读取过程中是否发生错误
if err != io.EOF { // ❼ 进一步检查是否到达文件末尾(EOF)
log.Fatal(err) // ❽ 当到达文件末尾时,记录错误并退出程序
}
break // ❾ 针对其他类型的错误,则退出 for 循环
}
}
}
本示例引入了一些将在后续章节中介绍的新特性,可以提前阅读以了解这些新特性。
首先,通过检查切片 os.Args 的长度来确保在命令行上指定了文件名。os.Args 的第一个值是程序自身的名称,其余的值是传递给程序的参数。检查 os.Args 的长度(❶)至少为 2,以确保为程序提供了参数。如果未提供参数,则使用 log.Fatal 函数(❷)打印一条消息并退出程序。接下来,使用 os.Open 函数(❸)获取只读文件句柄,Open 返回的第二个值是一个错误对象。如果打开文件失败,将打印错误消息并退出程序。
一旦成功获取了有效的文件句柄(文件描述符),则无论函数以何种方式退出,都必须在使用完该文件句柄后将其关闭。因此,在 main 函数内使用了 defer f.Close()(❹)语句,defer 关键字可确保在函数 main 返回之前执行 f.Close() 方法,从而避免了资源泄漏。通常情况下,函数(或方法)在被调用时都会立即执行,但是当使用 defer 关键字时,它会延迟函数(或方法)的调用,直到包含 defer 语句的函数(本例为 main)即将退出时才执行。这意味着,无论函数以何种方式退出,defer 后面的代码都会被执行。
将字节切片 data 传递给 f.Read 方法(❺),该方法会将文件内容读取到切片 data 中,同时会返回读取到切片中的字节数和一个错误值。如果读取过程中出现错误(❻),需要进一步检查是否到达文件末尾(❼)。如果是文件末尾,则用 break 语句退出 for 循环(❾)。对于其他类型的错误,则使用 log.Fatal 记录错误并立即退出程序(❽)。
关于 defer 关键字,还需要了解以下几个关键点:
- 支持多种可调用对象,包括函数、方法和闭包都可以与 defer 配合使用。无参数的闭包,可以捕获执行时的最新变量值。
- 多个 defer 语句的执行顺序是后进先出(LIFO)。这意味着,如果在一个函数内使用了多个 defer 语句,那么它们将被逆序执行(即最后注册的最先执行)。
- defer 语句执行时机与参数求值。
- 执行时机:在 return 语句之后,函数返回给调用方之前。
- 参数求值:defer 参数在注册时即确定值(值拷贝)。
这些特性使得 defer 既能保证资源释放的可靠性,又能灵活处理各种清理场景。如下示例展示了 defer 语句的上述特性:
func example() {
x := 1
defer fmt.Println("方式1(值拷贝):", x) // 立即拷贝 x=1
defer func() {
fmt.Println("方式2(闭包引用):", x) // 执行时才读取 x
}()
x = 100
}
运行该程序,将得到如下输出结果:
方式2(闭包引用): 100
方式1(值拷贝): 1
defer f(参数):参数在整个defer语句执行的那一刻就被计算并拷贝保存,即使参数后续被修改,f 执行时仍然是拷贝值。defer func(){...}()(闭包):闭包引用外部变量是,只有闭包实际执行时才去读取变量的当前值。
在 Go 中,你可以将带返回值的函数传给 defer,但这些返回值无法被外部代码获取。这是由 defer 的设计机制决定的。
func example() { defer func() int { // 将带返回值的函数传给 defer return 2 // 该返回值无法被外部代码获取 }() }
你可能会好奇:延迟执行的函数能否检查或修改其外围函数的返回值?答案是肯定的,这正是命名返回值(Named Return Values)最有价值的应用场景之一。这种机制允许在函数返回前,通过 defer 根据错误状态执行额外操作(如资源清理或错误增强)。在“九 错误处理<181页>”中,将介绍一种用 defer 向函数返回的错误添加上下文信息的编程模式。现在,看看如何使用命名返回值和 defer 来处理数据库事务的清理工作。
// 在数据库中执行插入操作,用命名返回值和defer来处理事务的提交和回滚
func DoSomeInserts(ctx context.Context, db *sql.DB, value1, value2 string)
(err error) {
tx, err := db.BeginTx(ctx, nil) // ❶ 使用默认隔离级别开始一个新事务,
if err != nil { // 如果开始数据库事务失败,直接返回错误。
return err
}
defer func() { // 延迟执行函数,用于在函数返回前提交或回滚事务。
if err == nil { // ❷ 如果执行过程中没有发生错误,尝试提交事务。
err = tx.Commit()
}
if err != nil { // ❸ 如果有错误发生,回滚事务。
tx.Rollback()
}
}() // ❹ 不要忘记此处的小括号,否则将引发编译错误
// 使用事务执行插入操作。
_, err = tx.ExecContext(ctx, "INSERT INTO FOO (val) values $1", value1)
if err != nil { // 如果插入操作失败,返回错误
return err
}
// 在这里可以使用 tx 执行更多的数据库插入操作。
return nil // 如果所有操作成功,返回 nil 表示没有错误。
}
虽然本书不深入讨论 Go 的数据库支持,但标准库 database/sql 提供了完善的数据库操作接口。在此示例中,(❶)创建了一个事务来执行一系列数据库插入操作。如果插入操作失败,则通过 defer 语句确保事务被回滚(不更改数据库)。如果所有操作都成功完成,则通过 defer 语句确保在函数返回前提交事务(保存数据库更改)。示例中使用 defer 和闭包(closures)来检查 err 是否已被赋值。如果未被赋值(未返回错误),则执行 tx.Commit 提交事务(❷),这同样可能返回一个错误。如果 db.BeginTx 返回了错误,err 的值将会被赋值,则调用 tx.Rollback 回滚事务(❸)。
Go 语言初学者常常会在为 defer 指定函数时,忘记在花括号后面加上小括号(❹)。
在 Go 语言中,一个常见的编程模式是,一个分配资源的函数同时返回一个用于清理资源的闭包。编写一个辅助函数来打开一个文件,并返回一个闭包(closures):
func getFile(name string) (*os.File, func(), error) {
file, err := os.Open(name)
if err != nil {
return nil, nil, err
}
return file, func() {
file.Close()
}, nil
}
辅助函数 getFile 返回一个指向 os.File 的指针,一个匿名函数(用于关闭文件),以及可能的错误。
在 main 函数中,使用 getFile 打开文件并将其返回值赋给变量:
// 返回一个指向 os.File 的指针、一个用于关闭文件的匿名函数、可能的错误
f, closer, err := getFile(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer closer() // 延迟执行 closer 函数,确保在 main 函数返回前关闭文件
因为 Go 语言不允许存在未使用的变量,所以从函数返回闭包(closures)意味着:如果闭包函数没有被调用,程序将无法编译。这会提醒用户使用 defer 语句调用闭包(closures)函数,执行资源清理。如前所述,当延迟执行闭包(closures)时,需要在闭包后面加上小括号。
评论