理解 Go 中的延迟

介绍

Go 有许多在其他编程语言中发现的常见的控制流关键字,如如果,交换,,等等.在大多数其他编程语言中找不到的一个关键字是defer,虽然它不太常见,但你很快就会看到它在你的程序中有多么有用。

一个推迟声明的主要用途之一是清理资源,如打开文件,网络连接和数据库处理(https://en.wikipedia.org/wiki/Handle_(computing)。当您的程序完成这些资源时,重要的是关闭它们以避免耗尽程序的限制,并允许其他程序访问这些资源。

在本文中,我们将学习如何正确地使用推迟声明来清理资源,以及使用推迟时犯的几个常见错误。

什么是推迟声明

一个推迟语句将推迟关键字之后的 函数调用添加到堆栈中.在它们被添加的函数返回时,该堆栈上的所有调用都被调用。

让我们看看推迟是如何工作的,通过打印一些文本:

1[label main.go]
2package main
3
4import "fmt"
5
6func main() {
7    defer fmt.Println("Bye")
8    fmt.Println("Hi")
9}

主要函数中,我们有两个陈述. 第一条陈述以推迟的关键字开始,然后是打印的陈述,打印出再见

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

1[secondary_label Output]
2Hi
3Bye

请注意,Hi是首先打印的,这是因为任何由defer关键字前面的陈述都不会被召唤,直到使用defer的函数结束。

让我们再来看看节目,这次我们将添加一些评论,以帮助说明正在发生的事情:

 1[label main.go]
 2package main
 3
 4import "fmt"
 5
 6func main() {
 7    // defer statement is executed, and places
 8    // fmt.Println("Bye") on a list to be executed prior to the function returning
 9    defer fmt.Println("Bye")
10
11    // The next line is executed immediately
12    fmt.Println("Hi")
13
14    // fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
15}

理解推迟的关键是,当执行推迟语句时,推迟函数的参数会立即被评估。

虽然这个代码说明了推迟会运行的顺序,但它不是在写Go程序时使用的典型方式,更有可能我们正在使用推迟来清理资源,例如文件处理器。

使用推迟来清理资源

首先,让我们看看一个程序,它写一个字符串到一个文件,但不使用defer来处理资源清理:

 1[label main.go]
 2package main
 3
 4import (
 5    "io"
 6    "log"
 7    "os"
 8)
 9
10func main() {
11    if err := write("readme.txt", "This is a readme file"); err != nil {
12    	log.Fatal("failed to write file:", err)
13    }
14}
15
16func write(fileName string, text string) error {
17    file, err := os.Create(fileName)
18    if err != nil {
19    	return err
20    }
21    _, err = io.WriteString(file, text)
22    if err != nil {
23    	return err
24    }
25    file.Close()
26    return nil
27}

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

虽然这个代码工作,但有一个微妙的错误.如果呼叫到 'io.WriteString' 失败,该函数将返回而不会关闭文件并将资源释放回系统。

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

 1[label main.go]
 2package main
 3
 4import (
 5    "io"
 6    "log"
 7    "os"
 8)
 9
10func main() {
11    if err := write("readme.txt", "This is a readme file"); err != nil {
12    	log.Fatal("failed to write file:", err)
13    }
14}
15
16func write(fileName string, text string) error {
17    file, err := os.Create(fileName)
18    if err != nil {
19    	return err
20    }
21    _, err = io.WriteString(file, text)
22    if err != nil {
23    	file.Close()
24    	return err
25    }
26    file.Close()
27    return nil
28}

现在,即使呼叫「io.WriteString」失败,我们仍然会关闭该文件,虽然这是一个相对容易发现和修复的错误,具有更复杂的功能,但它可能被错过了。

而不是将第二个呼叫添加到file.Close(),我们可以使用defer语句来确保无论在执行过程中采取哪些分支,我们总是呼叫Close()

以下是使用推迟关键字的版本:

 1[label main.go]
 2package main
 3
 4import (
 5    "io"
 6    "log"
 7    "os"
 8)
 9
10func main() {
11    if err := write("readme.txt", "This is a readme file"); err != nil {
12    	log.Fatal("failed to write file:", err)
13    }
14}
15
16func write(fileName string, text string) error {
17    file, err := os.Create(fileName)
18    if err != nil {
19    	return err
20    }
21    defer file.Close()
22    _, err = io.WriteString(file, text)
23    if err != nil {
24    	return err
25    }
26    return nil
27}

这次我们添加了代码行: defer file.Close(). 这告诉编译器在退出函数 write之前应该执行 file.Close

现在我们已经确保,即使我们在未来添加更多的代码并创建另一个分支,将始终清理和关闭该文件。

然而,我们通过添加推迟引入了另一个错误. 我们不再检查从关闭方法返回的潜在错误. 这是因为当我们使用推迟时,没有办法将任何返回值传递回我们的函数。

在 Go 中,在不影响程序的行为的情况下,多次拨打Close()被认为是安全且被接受的做法,如果Close()会返回错误,它会在首次拨打时这样做。

让我们看看我们如何推迟调用到关闭,如果遇到错误,我们仍然会报告错误。

 1[label main.go]
 2package main
 3
 4import (
 5    "io"
 6    "log"
 7    "os"
 8)
 9
10func main() {
11    if err := write("readme.txt", "This is a readme file"); err != nil {
12    	log.Fatal("failed to write file:", err)
13    }
14}
15
16func write(fileName string, text string) error {
17    file, err := os.Create(fileName)
18    if err != nil {
19    	return err
20    }
21    defer file.Close()
22    _, err = io.WriteString(file, text)
23    if err != nil {
24    	return err
25    }
26
27    return file.Close()
28}

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

然而,如果我们在函数中早些时候收到错误,例如当我们调用WriteString,函数将返回该错误,并且也会尝试调用file.Close,因为它被推迟了。

到目前为止,我们已经看到如何使用单个推迟来确保我们正确清理资源,接下来我们将看看如何使用多个推迟陈述来清理多个资源。

多重推迟声明

一个函数中有多个推迟陈述是正常的,让我们创建一个只有推迟陈述的程序,看看当我们引入多个推迟时会发生什么:

 1[label main.go]
 2package main
 3
 4import "fmt"
 5
 6func main() {
 7    defer fmt.Println("one")
 8    defer fmt.Println("two")
 9    defer fmt.Println("three")
10}

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

1[secondary_label Output]
2three
3two
4one

请注意,该顺序是我们称之为推迟陈述的相反顺序,这是因为呼叫的每一个推迟陈述都堆积在前一个的顶部,然后在函数离开范围(Last In, First Out)时反过来呼叫。

您可以在函数中需要的推迟呼叫数量尽可能多,但重要的是要记住,它们都将以相反的顺序进行呼叫。

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

 1[label main.go]
 2package main
 3
 4import (
 5    "fmt"
 6    "io"
 7    "log"
 8    "os"
 9)
10
11func main() {
12    if err := write("sample.txt", "This file contains some sample text."); err != nil {
13    	log.Fatal("failed to create file")
14    }
15
16    if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
17    	log.Fatal("failed to copy file: %s")
18    }
19}
20
21func write(fileName string, text string) error {
22    file, err := os.Create(fileName)
23    if err != nil {
24    	return err
25    }
26    defer file.Close()
27    _, err = io.WriteString(file, text)
28    if err != nil {
29    	return err
30    }
31
32    return file.Close()
33}
34
35func fileCopy(source string, destination string) error {
36    src, err := os.Open(source)
37    if err != nil {
38    	return err
39    }
40    defer src.Close()
41
42    dst, err := os.Create(destination)
43    if err != nil {
44    	return err
45    }
46    defer dst.Close()
47
48    n, err := io.Copy(dst, src)
49    if err != nil {
50    	return err
51    }
52    fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination)
53
54    if err := src.Close(); err != nil {
55    	return err
56    }
57
58    return dst.Close()
59}

我们添加了一个名为fileCopy的新函数. 在这个函数中,我们首先打开我们要复制的源文件. 我们检查是否收到打开文件的错误。

接下来我们创建目标文件. 再次,我们检查是否收到创建文件的错误. 如果是,我们返回错误并退出函数. 否则,我们也推迟目标文件的关闭()

现在我们有两个文件打开,我们将复制()从源文件的数据到目标文件. 如果成功,我们将尝试关闭两个文件. 如果我们收到一个错误试图关闭两个文件,我们将返回错误和退出函数范围。

请注意,我们为每个文件明确呼叫Close(),尽管推迟也会呼叫Close()。这就是为了确保如果有错误关闭一个文件,我们会报告错误。

结论

在本文中,我们了解了推迟声明,以及如何使用它来确保我们在我们的程序中正确清理系统资源。正确清理系统资源将使您的程序使用更少的内存并表现更好。

Published At
Categories with 技术
Tagged with
comments powered by Disqus