跳转到主要内容

介绍


Go 有许多其他编程语言中常见的控制流关键字,例如 if、switch、for 等。大多数其他编程语言中没有的一个关键字是 defer,虽然它不太常见,但您很快就会看到它在您的程序中有多大用处。

defer 语句的主要用途之一是清理资源,例如打开的文件、网络连接和数据库句柄。当您的程序使用完这些资源后,关闭它们以避免耗尽程序的限制并允许其他程序访问这些资源非常重要。 defer 通过将关闭文件/资源​​的调用保持在打开调用附近,使我们的代码更清晰,更不容易出错。

在本文中,我们将学习如何正确使用 defer 语句来清理资源以及使用 defer 时常犯的几个错误。

什么是延迟(defer )声明


defer 语句将 defer 关键字后面的函数调用添加到堆栈中。当添加它们的函数返回时,调用该堆栈上的所有调用。因为调用被放置在堆栈上,所以它们以后进先出的顺序被调用。

让我们通过打印一些文本来看看 defer 是如何工作的:

main.go

package main

import "fmt"

func main() {
    defer fmt.Println("Bye")
    fmt.Println("Hi")
}

在 main 函数中,我们有两个语句。第一条语句以 defer 关键字开始,然后是打印出 Bye 的 print 语句。下一行打印出 Hi。

如果我们运行程序,我们将看到以下输出:

Output
Hi
Bye


请注意,首先打印 Hi。这是因为在使用 defer 的函数结束之前,不会调用任何以 defer 关键字开头的语句。

让我们再看一下程序,这次我们将添加一些注释来帮助说明正在发生的事情:

package main

import "fmt"

func main() {
    // defer statement is executed, and places
    // fmt.Println("Bye") on a list to be executed prior to the function returning
    defer fmt.Println("Bye")

    // The next line is executed immediately
    fmt.Println("Hi")

    // fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
}

理解 defer 的关键是,当 defer 语句被执行时,defer 函数的参数会立即被求值。 当 defer 执行时,它将后面的语句放在一个列表中,以便在函数返回之前调用。

尽管这段代码说明了 defer 的运行顺序,但这并不是编写 Go 程序时使用的典型方式。 我们更有可能使用 defer 来清理资源,例如文件句柄。 接下来让我们看看如何做到这一点。

使用 defer 清理资源


使用 defer 清理资源在 Go 中很常见。 我们先来看一个将字符串写入文件但不使用 defer 处理资源清理的程序:

package main

import (
    "io"
    "log"
    "os"
)

func main() {
    if err := write("readme.txt", "This is a readme file"); err != nil {
        log.Fatal("failed to write file:", err)
    }
}

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    _, err = io.WriteString(file, text)
    if err != nil {
        return err
    }
    file.Close()
    return nil
}

在这个程序中,有一个名为 write 的函数将首先尝试创建一个文件。 如果它有错误,它将返回错误并退出函数。 接下来,它尝试将字符串 This is a readme file 写入指定文件。 如果收到错误,它将返回错误并退出函数。 然后,该函数将尝试关闭文件并将资源释放回系统。 最后,函数返回 nil 表示函数执行没有错误。

尽管此代码有效,但存在一个微妙的错误。 如果对 io.WriteString 的调用失败,该函数将返回而不关闭文件并将资源释放回系统。

我们可以通过添加另一个 file.Close() 语句来解决这个问题,这就是你可能会在没有 defer 的语言中解决这个问题的方法:

package main

import (
    "io"
    "log"
    "os"
)

func main() {
    if err := write("readme.txt", "This is a readme file"); err != nil {
        log.Fatal("failed to write file:", err)
    }
}

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    _, err = io.WriteString(file, text)
    if err != nil {
        file.Close()
        return err
    }
    file.Close()
    return nil
}

现在即使对 io.WriteString 的调用失败,我们仍然会关闭文件。 虽然这是一个相对容易发现和修复的错误,但功能更复杂,它可能被遗漏了。

我们可以使用 defer 语句来确保无论在执行期间采用哪些分支,我们始终调用 Close(),而不是添加对 file.Close() 的第二次调用。

这是使用 defer 关键字的版本:

package main

import (
    "io"
    "log"
    "os"
)

func main() {
    if err := write("readme.txt", "This is a readme file"); err != nil {
        log.Fatal("failed to write file:", err)
    }
}

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    defer file.Close()
    _, err = io.WriteString(file, text)
    if err != nil {
        return err
    }
    return nil
}

这次我们添加了这行代码:defer file.Close()。 这告诉编译器它应该执行文件。在退出函数写入之前关闭。

我们现在确保即使我们添加更多代码并创建另一个退出该函数的分支,我们也将始终清理并关闭该文件。

但是,我们通过添加延迟引入了另一个错误。 我们不再检查可以从 Close 方法返回的潜在错误。 这是因为当我们使用 defer 时,没有办法将任何返回值传回给我们的函数。

在 Go 中,在不影响程序行为的情况下多次调用 Close() 被认为是一种安全且可接受的做法。 如果 Close() 将返回错误,它将在第一次调用时返回。 这允许我们在函数的成功执行路径中显式调用它。

让我们看看我们如何既能推迟对 Close 的调用,又能在遇到错误时报告错误。

package main

import (
    "io"
    "log"
    "os"
)

func main() {
    if err := write("readme.txt", "This is a readme file"); err != nil {
        log.Fatal("failed to write file:", err)
    }
}

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    defer file.Close()
    _, err = io.WriteString(file, text)
    if err != nil {
        return err
    }

    return file.Close()
}

该程序中唯一的变化是我们返回 file.Close() 的最后一行。如果对 Close 的调用导致错误,现在将按预期将其返回给调用函数。请记住,我们的 defer file.Close() 语句也将在 return 语句之后运行。这意味着 file.Close() 可能会被调用两次。虽然这并不理想,但这是一种可以接受的做法,因为它不应该对您的程序产生任何副作用。

但是,如果我们在函数的早期收到错误,例如当我们调用 WriteString 时,该函数将返回该错误,并且还将尝试调用 file.Close,因为它被延迟了。尽管 file.Close 也可能(并且可能会)返回错误,但我们不再关心它,因为我们收到的错误更有可能告诉我们一开始出了什么问题。

到目前为止,我们已经看到了如何使用单个 defer 来确保我们正确地清理我们的资源。接下来我们将看到如何使用多个 defer 语句来清理多个资源。

多个 defer 语句


一个函数中有多个 defer 语句是正常的。让我们创建一个只有 defer 语句的程序,看看当我们引入多个 defer 时会发生什么:

package main

import "fmt"

func main() {
    defer fmt.Println("one")
    defer fmt.Println("two")
    defer fmt.Println("three")
}

如果我们运行程序,我们将收到以下输出:

Output
three
two
one


请注意,顺序与我们称为 defer 语句的顺序相反。 这是因为每个被调用的延迟语句都堆叠在前一个语句的顶部,然后在函数退出作用域时反向调用(后进先出)。

您可以在一个函数中根据需要有尽可能多的延迟调用,但重要的是要记住它们都将按照它们执行的相反顺序被调用。

现在我们了解了多个 defer 的执行顺序,让我们看看如何使用多个 defer 来清理多个资源。 我们将创建一个程序来打开一个文件,写入它,然后再次打开它以将内容复制到另一个文件。

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    if err := write("sample.txt", "This file contains some sample text."); err != nil {
        log.Fatal("failed to create file")
    }

    if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
        log.Fatal("failed to copy file: %s")
    }
}

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    defer file.Close()
    _, err = io.WriteString(file, text)
    if err != nil {
        return err
    }

    return file.Close()
}

func fileCopy(source string, destination string) error {
    src, err := os.Open(source)
    if err != nil {
        return err
    }
    defer src.Close()

    dst, err := os.Create(destination)
    if err != nil {
        return err
    }
    defer dst.Close()

    n, err := io.Copy(dst, src)
    if err != nil {
        return err
    }
    fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination)

    if err := src.Close(); err != nil {
        return err
    }

    return dst.Close()
}

我们添加了一个名为 fileCopy 的新函数。在此函数中,我们首先打开要从中复制的源文件。我们检查是否收到打开文件的错误。如果是这样,我们返回错误并退出函数。否则,我们推迟关闭刚刚打开的源文件。

接下来我们创建目标文件。同样,我们检查是否收到创建文件的错误。如果是这样,我们返回那个错误并退出函数。否则,我们也会推迟目标文件的 Close()。我们现在有两个 defer 函数,它们将在函数退出其作用域时被调用。

现在我们已经打开了两个文件,我们将 Copy() 将数据从源文件复制到目标文件。如果成功,我们将尝试关闭这两个文件。如果我们在尝试关闭任一文件时收到错误,我们将返回错误并退出函数范围。

请注意,我们为每个文件显式调用 Close(),即使 defer 也会调用 Close()。这是为了确保如果关闭文件时出现错误,我们会报告错误。它还确保如果由于任何原因函数因错误而提前退出,例如,如果我们未能在两个文件之间复制,则每个文件仍将尝试从延迟调用中正确关闭。

结论


在本文中,我们了解了 defer 语句,以及如何使用它来确保我们正确清理程序中的系统资源。正确清理系统资源将使您的程序使用更少的内存并更好地执行。要了解有关在何处使用 defer 的更多信息,请阅读有关处理恐慌的文章,或探索我们的整个如何在 Go 中编码系列。

 

文章链接