golang中你应该知道的defer知识(基础应用篇)

Go的“defer”:优雅代码的秘密

Life is what happens to you while you’re busy making other plans.
— John Lennon

golang defer (from github.com/MariaLetta/free-gophers-pack)

1.1 使用场景

  1. 资源清理:例如关闭文件、数据库链接或网络链接等。
1
2
3
4
5
6
7
8
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// do something
}
  1. 解锁资源:例如在并发编程中确保锁被解锁、执行wg.Done等。
1
2
3
4
5
6
7
8
9
10
11
12
var mu sync.Mutex

func criticalSection() {
mu.Lock()
defer mu.Unlock()
// do something
}

func worker(wg *sync.WaitGroup, id int) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
}
  1. 确保函数执行结束后进行后续操作:如记录日志或调试信息、性能分析等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"time"
)

// Records the execution time of a function
func timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
fmt.Printf("%s took %s\n", name, elapsed)
}

// Logs and tracks the execution of the function
func someFunction() {
defer timeTrack(time.Now(), "someFunction") // Records the function's execution time
defer fmt.Println("Function completed") // Logs the completion of the function

fmt.Println("Executing someFunction...")
time.Sleep(2 * time.Second)
// Simulating multiple return points
if true {
return // Due to defer, the cleanup or logging will still execute after this return
}
fmt.Println("This line won't be reached")
}

func main() {
someFunction()
}

它极大地简化了代码的编写和维护,减少了资源泄露和逻辑错误的风险。但是它也有一定的学习成本,值的我们深入了解它。此次我准备从官方文档入手,了解其基本的使用和背后的哲学,让我们开始吧。

1.2 触发时机

A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement[1]****, reached the end of its function body[2], or because the corresponding goroutine is panicking[3].
From: https://go.dev/ref/spec#Defer_statements[4]

  1. executed a return statement[5]
  2. reached the end of its **function body[6]*
  3. corresponding goroutine is panicking[7]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func deferFn1() {
defer fmt.Println("defer")
fmt.Println("func")
}
// output:
// func
// defer
func deferFn2() {
defer fmt.Println("defer")
return
}
// output:
// defer
func deferFn3() {
defer fmt.Println("defer")
panic("panic")
}
// output:
// defer
// panic stack

在Go语言中,defer语句会在包围它的函数返回之前执行。这意味着,无论函数怎样退出(正常返回或是由于panic导致的退出),只要 return or panic 前声明了defer语句,defer语句的执行都将被保证。

1.3 执行顺序与使用须知

  1. The expression must be a function or method call; it cannot be parenthesized. Calls of built-in functions are restricted as for expression statements[8].
  2. Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual[9] and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred.
    From: https://go.dev/ref/spec#Defer_statements[10]

defer语句的执行顺序上,Go语言有一个非常重要的特性:栈式调用(LIFO - Last In, First Out)。这一特性意味着,如果在一个函数中连续定义了多个defer语句,那么最后一个defer语句将最先执行。

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("end...")
}

// output
end...
3
2
1

另外defer具有一个特点,使用时必须要明白:the function value and parameters to the call are evaluated as usual[11] and saved anew but the actual function is not invoked.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
startedAt := time.Now()
fmt.Println(time.Since(startedAt))
time.Sleep(time.Second)
}
// output
0s

func main() {
startedAt := time.Now()
defer func() {
fmt.Println(time.Since(startedAt))
}()
time.Sleep(time.Second)
}
// output
1s

我们可以有两个角度来理解这两个例子:

  • ”捕获“时机:
    第一个例子,当函数被延迟时,它的参数在调用defer语句时立即求值,而不是在延迟函数实际执行时求值。这意味着在声明延迟的那一刻,startedAt的值就被“捕获”了,即使函数本身在稍后运行;
    而第二个例子中可以看到defer也适用于闭包(匿名函数),而闭包是在执行时从周围的词法作用域”捕获“变量,从而延迟更复杂的操作,所以此时的startedAt的值在执行时才被“捕获”。

  • 值拷贝:
    第一个例子值拷贝很容易理解;
    第二个例子我们可以这样理解:匿名函数本质上是一个闭包(closure),而它之所以可以捕获并引用其外部作用域中的变量,是因为当你使用 defer 关键字传递匿名函数时,Go 并没有拷贝整个函数的内容,而是将一个指向该匿名函数的指针存储起来,并在适当的时候调用它。
    因此,我们可以理解为拷贝的是函数指针,当调用时,外部作用域中的变量才确定下来。

我更喜欢第二种理解的方式,它加深了我对值拷贝的理解—当然也让我不用记那么多“伪概念”。

1.4 返回值处理

  1. if the surrounding function returns through an explicit return statement[12], deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller.
  2. if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned
  3. If the deferred function has any return values, they are discarded when the function completes
    From: https://go.dev/ref/spec#Defer_statements[13]

1.4.1 基本逻辑

if the surrounding function returns through an explicit return statement[14], deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller.

1
2
3
4
5
6
7
8
9
func deferReturnFn() int {
a := 1
defer func() {
a = 2
}()
return a
}
// output
1

这个结果返回为1,是不是觉得很奇怪?从官方的第一句话中你可以发现原因吗?我注意到 deferreturn 操作并不是原子性的,为了方便理解,我完全可以假设在执行 return前编译器会重新定义一个变量来接受当前的返回值:

1
2
3
4
5
6
7
8
func deferReturnFn() int {
a := 1
defer func() {
a = 2
}()
res := a
return res
}

这样就能理解为什么返回结果是 1 了。

1.4.2 named result parameters

if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned

第二句话强调了命名返回结果的情况,比如下面的例子,输出为2。

1
2
3
4
5
6
7
8
9
func deferReturnFn() (res int) {
res = 1
defer func() {
res = 2
}()
return
}
// output
2

猛然一看,好像觉得很奇怪。但是和1.4.1 相比,这里的唯一区别就是返回变量已经提前定义好。此时我觉得可以理解为 deferreturn 操作是原子性的了,所以这个例子的结果是2。

1.4.3 特殊case

If the deferred function has any return values, they are discarded when the function completes

1
2
3
4
5
6
7
8
9
10
func deferReturnFn() int {
res := 1
defer func() int {
res++
return res
}()
return res
}
// output
1

此时匿名函数的结果并没有接收者,所以被 discarded 很合理。

1.5 使用注意点

除了上面说的几个注意点:

  1. 栈式调用
  2. 匿名函数
  3. 返回值处理
    我们还应该注意defer的“摆放位置”,避免函数退出时,资源未释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// good
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// do something
}
// bad
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
// do something
defer file.Close()
}

1.6 哲学思想

从资源清理来思考defer 设计的哲学,例子来源于 11 | 程序中的错误处理:错误返回码和异常捕捉-左耳听风-极客时间[15]

我们都知道,程序出错时需要对已分配的一些资源做清理,在传统的玩法下,每一步的错误都要去清理前面已分配好的资源。于是就出现了 goto fail 这样的错误处理模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>

#define FREE(p) if(p) { \
free(p); \
p = NULL; \
}

void ReportError(int errorCode) {
// Error handling code here
printf("Error: %d\n", errorCode);
}

int main() {
char *fname = NULL, *lname = NULL, *mname = NULL;

fname = (char*) calloc(20, sizeof(char));
if (fname == NULL) {
goto fail;
}

lname = (char*) calloc(20, sizeof(char));
if (lname == NULL) {
goto fail;
}

mname = (char*) calloc(20, sizeof(char));
if (mname == NULL) {
goto fail;
}

// Normal processing would happen here
// ...

fail:
FREE(fname);
FREE(lname);
FREE(mname);
ReportError(1); // Example error code

return 0;
}

这样的处理方式虽然可以,但是会有潜在的问题。最主要的一个问题就是你不能在中间的代码中有 return 语句,因为你需要清理资源。在维护这样的代码时需要格外小心,因为一不注意就会导致代码有资源泄漏的问题。

于是,C++ 的 RAII(Resource Acquisition Is Initialization)机制使用面向对象的特性可以容易地处理这个事情。RAII 其实使用 C++ 类的机制,在构造函数中分配资源,在析构函数中释放资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <mutex>

// First, declare an RAII class. Pay attention to the constructor and destructor.
class LockGuard {
public:
LockGuard(std::mutex &m) : _m(m) {
m.lock(); // Lock the mutex when the LockGuard is constructed
}

~LockGuard() {
_m.unlock(); // Unlock the mutex when the LockGuard is destroyed
}

private:
std::mutex& _m;
};

// Now, let's see how to use it
void good() {
LockGuard lg(m); // RAII class: mutex is locked upon construction
f(); // If f() throws an exception, the mutex will be released
if (!everything_ok()) return; // Early return, mutex will be released when LockGuard is destroyed
} // If good() returns normally, the mutex is released

而在 Go 语言中,并没有class 的关键字,而是使用defer关键字做到这样的效果,显然这种方式要比goto fail 更好,能降低程序员的心智负担。

Go语言在设计时显然吸收过前人的经验,从资源清理的这一点背景中,我们可以管中窥豹,了解defer的设计哲学。我看过不少人写Go的代码有其他语言的痕迹,这恰巧说明他们并不懂得Go的设计思想,所以才会出现他们写Go代码时磕磕绊绊,阅读他们的Go代码觉得奇怪的双重困境。

1.7 总结

本文从Go defer官方文档说明着手,从使用现象举例子探究defer的使用,介绍了以下内容:

  1. 触发时机
  2. 执行顺序与使用须知
  3. 返回值处理
  4. 使用注意点

最后引出对defer 设计的思考,defer 是一个好的关键字,理解它、掌握它,它会帮助我们写出优雅的Go代码。

当然本文都是从现象入手,现在还有下面两个关键问题:

  1. defer的性能怎么样?Go team 做了哪些优化?
  2. defer 的设计哲学从源码层面是如何实现的?

这两个问题想给出答案确实比较困难,但这也意味着更多的收获,希望之后我能有精力探究其中的奥秘。

1.8 引用链接

[1] return statement: https://go.dev/ref/spec#Return_statements
[2] function body: https://go.dev/ref/spec#Function_declarations
[3] panicking: https://go.dev/ref/spec#Handling_panics
[4]: https://go.dev/ref/spec#Defer_statements
[8] expression statements: https://go.dev/ref/spec#Expression_statements
[9] evaluated as usual: https://go.dev/ref/spec#Calls
[15] 11 | 程序中的错误处理:错误返回码和异常捕捉-左耳听风-极客时间: https://time.geekbang.org/column/article/675


更多该系列文章,参考medium链接:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

English post: https://programmerscareer.com/golang-defer/
作者:Wesley Wei – Twitter Wesley Wei – Medium
发表日期:原文在 2024-09-08 21:17 时创作于 https://programmerscareer.com/zh-cn/golang-defer/。
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

golang select 的使用及基本实现 [置顶🔝]一些推广信息

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×