defer 延迟调用【GO 基础】

〇、前言

在 Go 语言中,defer 是一种用于延迟调用的关键字。

defer 在 Go 语言中的地位非常重要,它是确保资源正确释放和程序健壮性的关键字。

本文将通过示例对其进行专门的详解。

一、defer 简介

defer 的主要用途是在函数执行完毕之前,确保某个操作被执行

通常用于:资源的释放管理,如关闭文件句柄、释放撕资源、网络连接、数据库连接等。

以下是 defer 的一些关键特性:

  • 延迟执行:defer 后的函数调用不会立即执行,而是会推迟到包含它的函数执行结束时才执行
  • 逆序执行:如果有多个 defer 语句,它们会按照后进先出的顺序执行,即最后一个被 defer 的语句会最先执行
  • 作用域限制:defer 的作用域仅限于包围它的函数,不同于其他语言中的 finally 关键字,后者的作用域在其异常块中
  • 语法简洁:在语法上,defer 与普通的函数调用没有区别,但是它提供了一种优雅的方式来确保资源的释放
  • 资源管理:defer 常用于确保文件、网络连接等资源在使用完毕后被正确关闭,避免资源泄露。

在实际编程中,合理使用 defer 可以有效地提高代码的可读性和可维护性。下面会进行详细介绍。

二、defer 的使用

2.1 多 defer 时遵循先进后出 FILO

这个不难理解,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。

下面是一段示例代码,0~4 依次执行 defer,查看输出:

复制代码
package main

import "fmt"

func main() {
	fmt.Println("begin-------------------!")
	var whatever [5]struct{}
	for i := range whatever {
		defer fmt.Println(i)
	}
	fmt.Println("end---------------------!")
}

输出结果是从 4 开始,说明先运行的 defer 后输出结果,且主线程先运行完成:

2.2 defer 与闭包

先解释一下闭包: 它是一种编程概念,允许一个函数访问并操作其外部作用域中的变量 。闭包通常出现在函数嵌套的情况下,即一个函数内部定义了另一个函数。内部的这个函数能够访问所在函数的变量和参数,无论外部函数已经执行完毕。 这种结构使得内部函数保留对外部函数变量的引用,因此这些变量不会被垃圾回收机制收回

闭包是由函数及其相关的引用环境组合而成的实体,可以理解为函数加上它所引用的外部变量 。在Go语言中,所有的闭包都是匿名函数,但不是所有的匿名函数都是闭包。只有当匿名函数捕获了其外部作用域中的变量时,它才成为一个闭包。

官方对 defer 有句解释:

  • Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.
  • 每次执行"defer"语句时,函数值和调用的参数都会像往常一样求值并重新保存,但不会调用实际的函数。

如下一段示例代码,将上一章节的代码 defer 的内容换成一个函数:

复制代码
package main

import "fmt"

func main() {
	fmt.Println("begin-------------------!")
	var whatever [5]struct{}
	for i := range whatever {
		defer func() { fmt.Println(i) }()
	}
	fmt.Println("end---------------------!")
}

输出结果变成全部为 4,而非 4~0:

函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成 4,所以输出全都是 4。

下面将上边示例改良一下,在循环中添加新的变量暂存循环序列值 i

复制代码
package main

import "fmt"

func main() {
	fmt.Println("begin-------------------!")
	var whatever [5]struct{}
	for i := range whatever {
		ii := i // 看似多余的赋值,实际上是将公用变量赋值给局部变量
		defer func() { fmt.Println(ii) }()
	}
	fmt.Println("end---------------------!")
}

运行结果:

2.3 某个 defer 发生异常,不会影响其他 defer 的正常执行

如下代码,在 b 和 c 之间的 defer 异常,但不影响 a 和 b 的正常执行:

复制代码
package main

func test(x int) {
    defer println("a")
    defer println("b")

    defer func() {
        println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
    }()

    defer println("c")
}

func main() {
    test(0)
}

2.4 延迟调用参数在注册时求值或复制,可用闭包 "延迟" 读取

如下代码,在 x 声明之后,将其作为参数传入 defer 函数体,此时 x 被赋值,后续的值变化将不会影响 defer 的输出:

复制代码
package main

func test() {
	x, y := 10, 20

	defer func(i int) {
		println("defer:", i, y) // y 闭包引用
	}(x) // x 被复制

	x += 10
	y += 100
	println("x =", x, "y =", y)
}

func main() {
	test()
}

2.5 大循环时,用和不用 defer 的性能比较

如下代码,声明两个互斥锁,分别加锁和关锁,一个使用 defer 关锁,在循环次数较大时看下耗时情况:

复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

var lock sync.Mutex
var lock1 sync.Mutex

func test() {
	lock.Lock()
	lock.Unlock()
}

func testdefer() {
	lock1.Lock()
	defer lock1.Unlock()
}

func main() {
	func() {
		t1 := time.Now()

		for i := 0; i < 1000000; i++ {
			test()
		}
		elapsed := time.Since(t1)
		fmt.Println("test elapsed: ", elapsed)
	}()
	func() {
		t1 := time.Now()

		for i := 0; i < 1000000; i++ {
			testdefer()
		}
		elapsed := time.Since(t1)
		fmt.Println("testdefer elapsed: ", elapsed)
	}()
}

如下图测试结果,在一百万次时,起始差别也不太大:

三、defer 陷阱

3.1 执行闭包(Closure)时的特殊取值

一般情况下,defer 执行的代码行,是参数预加载的,也就是取当前行之前的值,之后变量值变化无影响。

但 defer 执行闭包就不一样了,会取之后代码执行完成后变量最终的值。

复制代码
package main

import (
	"errors"
	"fmt"
)

func foo(a, b int) (i int, err error) {
	defer fmt.Printf("first defer err %v\n", err)
	// 普通的即时调用函数
	defer func(err error) {
		fmt.Printf("second defer err %v\n", err)
	}(err)
	// 此处为闭包,函数内部引用了外部的变量
	defer func() {
		fmt.Printf("third defer err %v\n", err)
	}()
	if b == 0 {
		err = errors.New("divided by zero!")
		return // 异常直接返回
	}
	i = a / b
	return
}

func main() {
	foo(2, 0)
}

// 执行结果,第三个 defer 能取到后续代码赋值给 err 的异常信息
third defer err divided by zero!
second defer err <nil>
first defer err <nil>

3.2 return 的使用

如下示例,函数 foo() 返回值是整数变量 i,第一次赋值是在 i=0 位置,在最后的 return 时,进行了第二次赋值,defer 执行的闭包是一直监视着变量 i 值的变化

复制代码
package main

import "fmt"

func foo() (i int) {

	i = 0

	defer func() {
		fmt.Println("defer-i:", i)
	}()
	fmt.Println("return-before-i:", i)
	return 2
}

func main() {
	foo()
}

// 结果输出,由于 defer 是在全部代码执行完成后(包括 return)才运行
// 因此,defer 最后读取的是 return 赋值给 i 的值 2
return-before-i: 0
defer-i: 2

另外,defer 不可在有 return 分支的后边使用,这样可能造成 defer 为生效,达不到预期效果。这个很好理解,程序走到 return 后,就直接返回了,后边的代码就不再执行了。

3.3 延迟调用的函数为 nil 时引起 panic

如下示例代码,通过 recover() 函数捕捉 panic 信息:

注:recover() 函数的主要作用是在程序发生 panic 时,能够在 defer 语句中捕获到 panic 的信息 ,并返回导致 panic 的错误值。这样程序可以据此进行相应的处理,而不是直接崩溃退出

复制代码
package main

import (
	"fmt"
)

func test() {
	var run func() = nil
	defer run()
	fmt.Println("runs")
}

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
		fmt.Println("end------------!")
	}()
	fmt.Println("start----------!")
	test()
}

// 结果输出,最后的 end 记录可正常输出,说明程序没有异常退出
start----------!
runs
runtime error: invalid memory address or nil pointer dereference
end------------!

3.4 在 for 循环中调用 defer 并非立即执行

如下图中的示例代码,在 for 循环中每次加载 defer,但是并非每次都会执行,而是在 for 循环完成后一块执行:

这些延迟函数会不停地堆积到延迟调用栈中,最终可能会导致一些不可预知的问题。

解决方案有两种,如下:

1)直接在每次循环完成后,去掉 defer 关键字,直接执行 row.Close():

复制代码
package main

import (
	"database/sql"
	"fmt"
	"time"

	_ "github.com/denisenkom/go-mssqldb" //  go get github.com/denisenkom/go-mssqldb
)

func main() {
	func() {
		connStr := "server=<服务器地址>;user id=<用户名>;password=<密码>;database=<数据库名>;encrypt=disable"
		// 连接到SQL Server
		db, err := sql.Open("mssql", connStr)
		if err != nil {
			fmt.Println("db,err:", err)
			return
		}
		for {
			row, err := db.Query("select ID,shujubs from tablename where shujubs like '20240328%'")
			if err != nil {
				fmt.Println("row,err:", err)
			}
			row.Close()
		}
	}()
}

2)将 for 内循环体包含在新的函数中,在函数内使用 defer:

复制代码
package main

import (
	"database/sql"
	"fmt"
	"time"

	_ "github.com/denisenkom/go-mssqldb" //  go get github.com/denisenkom/go-mssqldb
)

func main() {
	func() {
		connStr := "server=<服务器地址>;user id=<用户名>;password=<密码>;database=<数据库名>;encrypt=disable"
		// 连接到SQL Server
		db, err := sql.Open("mssql", connStr)
		if err != nil {
			fmt.Println("db,err:", err)
			return
		}
		for {
			func() {
				row, err := db.Query("select ID,shujubs from tablename where shujubs like '20240328%'")
				if err != nil {
					fmt.Println("row,err:", err)
				}
				defer row.Close() // 函数体中代码执行完后,就会执行 defer
			}()
		}
	}()
}

3.5 在用 {} 包裹的代码块中使用 defer 是没有效果的

首先要知道的是,defer 延迟执行是相对于函数的,而非代码块。

如下代码来测试下,在独立代码块中使用 defer:

复制代码
package main

import "fmt"

func main() {
	fmt.Println("start----------!")
	{ // 独立代码块
		defer func() {
			fmt.Println("block: defer runs")
		}()

		fmt.Println("block: ends")
	}
	fmt.Println("main: ends")
	fmt.Println("end------------!")
}

如下输出结果,代码块中的 defer 内容,并没有在"main: ends"之前执行:

那么下边来优化下:

复制代码
package main

import "fmt"

func main() {
	fmt.Println("start----------!")
	func() { // 将独立代码块,改造成函数
		defer func() {
			fmt.Println("block: defer runs")
		}()

		fmt.Println("block: ends")
	}()
	fmt.Println("main: ends")
	fmt.Println("end------------!")
}

3.6 使用指针对象作为接收者和不用指针的对比

先看下不用指针的情况:

复制代码
package main

import "fmt"

type Car struct {
	model string
}

func (c Car) PrintModel() { // 不用指针对象
	fmt.Println(c.model)
}

func main() {
	fmt.Println("start----------!")
	c := Car{model: "DeLorean DMC-12"}
	defer c.PrintModel() // 此时就直接将对象 c 的值保存下来
	c.model = "Chevrolet Impala" // 此处修改对上边已保存的 c 对象无效
	fmt.Println("end------------!")
}

下面用指针对象试试看:

复制代码
package main

import "fmt"

type Car struct {
	model string
}

func (c *Car) PrintModel() { // 使用指针对象
	fmt.Println(c.model)
}

func main() {
	fmt.Println("start----------!")
	c := Car{model: "DeLorean DMC-12"}
	defer c.PrintModel()         // 这里只保存了对象 c 的物理地址
	c.model = "Chevrolet Impala" // 修改对象 c 后,defer 仍可取最新值
	fmt.Println("end------------!")
}

参考:http://www.topgoer.com/%E5%87%BD%E6%95%B0/%E5%BB%B6%E8%BF%9F%E8%B0%83%E7%94%A8defer.html https://studygolang.com/articles/12061#commentForm

相关推荐
梦想很大很大11 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰16 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘20 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤20 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想