Bootstrap

defer的作用与设计思想

新年的第一天,迎着清晨的阳光,继续构筑我的魅力golang,今天带来的是defer,defer是golang独有的一个特殊的关健字,用于延迟函数的执行。它通常与资源清理、错误处理以及程序退出相关联,是 golang简洁性和可读性的重要体现。通过 defer,开发者可以将需要延迟执行的逻辑嵌入代码中,避免复杂的资源清理代码。

1defer 的作用

defer 的主要作用是延迟执行一个函数或方法,直到包围它的函数执行结束为止。被延迟的函数会按照后进先出(LIFO,Last In First Out)的顺序执行。

基本语法

defer funcName(args)

当 defer 被调用时,其后的函数会被推入一个延迟栈中,并在当前函数返回时按照后进先出的顺序执行。

基本用法

package main

import "fmt"

func main() {
	fmt.Println("Start")

	defer fmt.Println("Deferred 1")
	defer fmt.Println("Deferred 2")

	fmt.Println("End")
}

输出结果

StartEnd

Deferred 2

Deferred 1

多个 defer 的执行顺序

多个 defer 按照后进先出的顺序执行。上述代码中,Deferred 2 先被推入栈,因此最后执行。

2defer 的设计思想

defer 的设计思想源于 golang的简洁性和可维护性,目的是提供一种清晰、安全、易用的资源管理方式。其设计理念包括以下几个方面:

2.1 简化资源管理

在许多编程语言中,资源(如文件、网络连接、内存等)的管理通常通过显式的释放操作实现,但容易遗漏,导致资源泄漏。defer 将资源释放的代码直接写在资源分配之后,提高了代码的可读性和健壮性。

示例:文件操作

package main

import (
	"fmt"

	"os"
)

func main() {
	file, err := os.Open("example.txt") // 打开文件

	if err != nil { // 如果发生错误,输出错误信息并返回

		fmt.Println("Error opening file:", err)

		return
	}

	defer file.Close() // 确保资源在函数结束时被释放

	// 文件操作
	fmt.Println("File opened successfully")
}

在上述代码中,无论程序是否发生错误,file.Close() 都会被执行,避免资源泄漏。

2.2 提供一致的错误处理

Go 中的错误处理采用显式返回错误的方式。通过 defer,开发者可以在函数结束时统一检查错误或执行恢复逻辑。

示例:错误恢复

package main

import "fmt"

func recoverFromPanic() { //如果捕获到异常,从此处恢复
	if r := recover(); r != nil { //判断recover是否捕获到异常

		fmt.Println("Recovered from panic:", r)

	}
}

func mightPanic() { //可能抛出异常
	defer recoverFromPanic() // 捕获异常

	panic("Something went wrong!")
}

func main() {
	fmt.Println("Starting program")

	mightPanic() //抛出异常

	fmt.Println("Program continues...")
}

2.3 强调代码简洁性

传统的资源管理通常需要多次显式调用清理代码,而 defer 将清理逻辑集中在资源分配附近,使代码更易于理解和维护。

3defer 的内部实现

defer 的实现依赖于一个延迟栈(deferred stack)。当调用 defer 时,Go 会将被延迟的函数及其参数存储在栈中,并在函数结束时按照后进先出的顺序执行。

参数的求值时机

被延迟的函数参数会在 defer 声明时立即求值,而非函数执行时。

示例:

package main

import "fmt"

func main() {
	x := 10
	defer fmt.Println("Deferred value:", x)

	x = 20
	fmt.Println("Current value:", x)
}

输出结果

Current value: 20

Deferred value: 10

原因是 defer 声明时,x 的值已经被捕获。

4defer 的常见应用场景

4.1文件或资源清理

defer 常用于文件、网络连接、数据库连接等资源的清理。

示例:文件清理

package main

import (
	"bufio"

	"fmt"

	"os"
)

func readFile(filename string) {   //读取文件函数
	file, err := os.Open(filename)  //打开文件

	if err != nil {  //判断文件是否存在
		fmt.Println("Error:", err)

		return
	}

	defer file.Close()  //延迟关闭文件

	scanner := bufio.NewScanner(file)  //创建一个扫描文件的对象

	for scanner.Scan() {   //循环读取文件

		fmt.Println(scanner.Text())

	}
}

4.2 错误恢复

结合 panic 和 recover,defer 可以实现错误恢复,防止程序崩溃。

示例:错误恢复

package main

import "fmt"

func safeDivision(a, b int) { //创建一个安全除法函数
	defer func() { //defer关键字用于延迟执行一个函数

		if r := recover(); r != nil { //recover函数用于捕获panic异常
			fmt.Println("Recovered from panic:", r)
		}
	}()

	fmt.Println("Result:", a/b)
}

func main() {
	safeDivision(10, 0) //调用安全除法函数,传入非法除数0

	fmt.Println("Program continues...")
}

输出:

Recovered from panic: runtime error: integer divide by zero
Program continues...

4.3 日志跟踪

defer 可用于记录函数的进入和退出时间,以便调试和性能分析。

示例:日志跟踪

package main

import (
	"fmt"

	"time"
)

func trace(msg string) func() { //创建一个跟踪msg 相关的函数
	start := time.Now() //记录开始时间

	fmt.Println("Entering:", msg)

	return func() { //返回一个函数
		fmt.Println("Exiting:", msg)

		fmt.Println("Elapsed time:", time.Since(start))
	}
}

func someFunction() {
	defer trace("someFunction")() //defer延迟调用

	fmt.Println("In someFunction")

	time.Sleep(2 * time.Second) //延时2秒
}

func main() {
	someFunction()
}

输出结果

Entering: someFunction
In someFunction
Exiting: someFunction
Elapsed time: 2.0104964s

5defer 的限制与注意事项

首先是参数求值时机,如前所述,defer 的参数在声明时即被求值,因此后续对参数的修改不会影响被延迟的函数。

然后就是性能开销,defer 有一定的性能开销,尤其是在高频调用中。对于性能敏感的场景,应尽量避免在循环内频繁使用 defer。

优化建议

package main

import "fmt"

func main() {
	for i := 0; i < 10; i++ {  

		func() {

			fmt.Println("Deferred:", i) // 推荐

		}()
	}
}

defer 设计上强调代码的可读性和资源管理的安全性。尽管 defer 有一些性能限制,但其在绝大多数场景中的实用性远远超过这些限制。合理地使用 defer,可以显著提升代码的清晰度和健壮性。

最后,祝大家新年新气象,期待自己与大家一起探索更多未知,拓宽技术,用代码书写梦想,用努力拥抱可能!

;