理解Go中的延迟

来自菜鸟教程
跳转至:导航、​搜索

介绍

Go 有许多其他编程语言中常见的控制流关键字,例如 ifswitchfor 等。 在大多数其他编程语言中找不到的一个关键字是 defer,尽管它不太常见,但您会很快看到它在您的程序中的用处。

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

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

什么是 defer 语句

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

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

main.go

package main

import "fmt"

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

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

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

OutputHi
Bye

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

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

main.go

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 的运行顺序,但这并不是编写 Go 程序时使用的典型方式。 我们更有可能使用 defer 来清理资源,例如文件句柄。 接下来让我们看看如何做到这一点。

使用defer清理资源

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

main.go

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 的语言中解决这个问题的方法:

main.go

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 失败,我们仍然会关闭文件。 虽然这是一个相对容易发现和修复的错误,但功能更复杂,它可能被遗漏了。

除了将第二次调用添加到 file.Close() 之外,我们还可以使用 defer 语句来确保无论执行期间采用哪些分支,我们始终调用 Close()

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

main.go

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()。 这告诉编译器它应该在退出函数 write 之前执行 file.Close

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

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

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

让我们看看我们如何既可以 defer 调用 Close,并且如果遇到错误仍然报告错误。

main.go

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 时会发生什么:

main.go

package main

import "fmt"

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

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

Outputthree
two
one

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

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

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

main.go

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 的新函数。 在此函数中,我们首先打开要从中复制的源文件。 我们检查是否收到打开文件的错误。 如果是这样,我们 return 错误并退出函数。 否则,我们 defer 关闭我们刚刚打开的源文件。

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

现在我们打开了两个文件,我们将 Copy() 将源文件中的数据传输到目标文件。 如果成功,我们将尝试关闭这两个文件。 如果我们在尝试关闭任一文件时收到错误,我们将 return 错误并退出函数范围。

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

结论

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