返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

4.2 延迟调用

发布于 2024-10-12 19:15:47 字数 3378 浏览 0 评论 0 收藏 0

语句 defer 注册 稍后执行的函数调用。这些调用被称作 延迟调用 ,因为它们直到当前函数结束前( RET )才被执行。常用于资源释放、锁定解除,以及错误处理等操作。

  • 注册非 nil 函数,复制执行所需参数。
  • 多个延迟调用按 FILO 次序执行。
  • 运行时确保延迟调用总被执行。( os.Exit 除外)

更详细的实现分析,请阅读《下卷: 8.6 延迟调用》。

func main() {
	f, err := os.Open("./main.go")
	if err != nil {
		log.Fatalln(err)
	}
    
	defer f.Close()
	
	b, err := io.ReadAll(f)
	if err != nil {
		log.Fatalln(err)
	}

	println(string(b))
}
func main() {
	defer println(1)   // 此处是注册,而非执行。
	defer println(2)   // 注册需提供执行所需参数。
	defer println(3)   // 按 FILO 次序执行。

	println("main")    
}                      // 退出前执行 defer。

/*

main
3
2
1

*/
func main() {
	var f func()

	// defer f()
	// ~~~~~~~~~ panic: invalid memory address or nil pointer dereference
}

正常退出,或以 panic 中断,运行时都会确保延迟函数被执行。

func main() {
	defer println("defer!")
	panic("panic!")          // 换成 os.Exit,会立即终止进程,
}                            // 肯定不执行延迟调用。

/*

defer!
panic: panic!

*/

注意参数复制行为,必要时以指针或引用类型代替。

func main() {
	x := 100
	defer println("defer:", x)

	x++
	println("main:", x)
}

/*

 main: 101
defer: 100

*/

返回值

延迟调用以闭包形式修改命名返回值,但须注意执行次序。

func test() (z int) {
	defer func() {
		z += 200
	}()
    
    return 100   // return = (ret_val_z = 100, call defer, ret)
}

func main() {
	println(test())  // 300
}

/*

$ go build -gcflags "-S"

--- test -----------------------

	TEXT	test(SB)

z = 0

	MOVQ	$0, z+8(SP)           +8 +---------+
                                     | z       |
	LEAQ	test.func1(SB), AX    16 +---------+
	MOVQ	AX, +16(SP)              | func1   |<--+
                                  24 +---------+   |
	LEAQ	z+8(SP), AX              | &z      |   |
	MOVQ	AX, 24(SP)            32 +---------+   |
                                     | &func1 -|---+
	LEAQ	16(SP), AX               +---------+
	MOVQ	AX, 32(SP)

z = 100

	MOVQ	$100, z+8(SP)

call defer_func1

	MOVQ	32(SP), DX
	MOVQ	(DX), AX
	CALL	AX

ret z

	MOVQ	z+8(SP), AX
	RET

--- test.func1 -------------------

	TEXT	test.func1(SB)

	MOVQ	8(DX), AX
	ADDQ	$200, (AX)

	RET

*/

为阅读方便,反汇编结果进行了调整。且所生成指令与编译器版本和优化有关,以实际输出为准。

从结果看,内联效果很好。从这点上说, defer 不再是严重的性能关注点。

如果不是命名返回值,那么结果截然不同。

func test() int {
	z := 0

	defer func() {
		z += 200    // 本地变量,与返回值无关。
	}()
    
    z = 100
    return z        // return = (ret_val = 100, call defer, ret)
}

func main() {
	println(test())  // 100
}

误用

千万记住,延迟调用是在函数结束时才被执行。不合理的使用方式会浪费更多资源,甚至造成逻辑错误。

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

		path := fmt.Sprintf("./log/%d.txt", i)

		f, err := os.Open(path)
		if err != nil {
			log.Println(err)
			continue
		}
        
		// 注册的 Close 直到函数结束时才执行,而非本次循环周期。
		// 这无端延长了 10000 个 f 生命周期,平白消耗资源。
        
		defer f.Close()
        
		//... do something ...
	}
}

应该直接调用,或重构为函数,让循环和处理算法分离。

func main() {

	analyze = func(id int) {
		path := fmt.Sprintf("./log/%d.txt", id)

		f, err := os.Open(path)
		if err != nil {
			log.Println(err)
			return
		}
        
        // 现在不是问题了。
		defer f.Close()
        
		//... do something ...		
	}

	for i := 0; i < 10000; i++ {
		analyze(i)
	}
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文