介绍
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()
。这就是为了确保如果有错误关闭一个文件,我们会报告错误。
结论
在本文中,我们了解了推迟
声明,以及如何使用它来确保我们在我们的程序中正确清理系统资源。正确清理系统资源将使您的程序使用更少的内存并表现更好。