你是否了解 defer 的特性
defer
函数是 go
语言用来延迟执行的语句,defer
语句会在函数返回之前执行,可以用来释放资源、解锁、关闭文件等操作
关于 defer
的一些特性,你是否足够了解?
defer 表达式会影响 return 的返回值
函数 fn
返回结果是啥?
func fn(n int) (r int) {
defer func() {
r += n
}()
r = n + n
return
}
fmt.Println(fn(2)) // ?
函数 fn2
返回结果又是啥
func fn2(n int) int {
var a int
defer func() {
a += n
}()
a = n + n
return a
}
fmt.Println(fn2(2)) // ?
答案是:fn
函数的返回结果是 6
,fn2
函数的返回结果是 4
为什么 fn
和 fn2
两个函数的返回结果不一样呢?
因为使用命名返回值 (r int)
相当于返回了 r
的地址(实际不是),但是返回值是 int
,表示返回值已经确定是值,不是地址(在编译阶段确定的)
虽然 fn2
函数在 defer
中修改了 a
的值,但是返回值在编译阶段已经确定了,所以 fn2
函数的返回结果不受 defer
影响
我们再来看一个例子 fn3
func fn3(n int) *int {
a := new(int)
defer func() {
*a = *a + n
}()
*a = n + n
return a
}
fmt.Println(fn3(2)) // 6
我们将 fn3
的返回值修改为 *int
,defer
函数就能影响返回值了
defer 语句修改返回值
有了上面的例子,我们再来看下面两个例子
fn
和 fn2
函数的返回结果是啥?
func fn(n int) (r int) {
defer func() {
r = 2
}()
return 1
}
func fn2(n int) (r int) {
r = 2
return 1
}
fn
函数的返回结果是 2
,fn2
函数的返回结果是 1
为什么会出现这种现象呢?
return
是函数最后执行的语句,所以 return
后面表达式的结果会被赋值给命名返回值 (r int)
,所以 fn2
的返回结果是 1
但是 defer
语句是在 return
之后执行的,仅管已经确定了返回值,但由于是命名返回值,所以 defer
语句可以修改返回值,所以 fn
的返回结果是 2
defer 执行时机
defer
和 return
谁先执行?
你可以会想到这样写
func main() {
fmt.Println(fn())
}
func fn() string {
defer func() {
fmt.Println("defer")
}()
return "return"
}
输出结果是
// "defer"
// "return"
你可能会觉得 defer
是在 return
之前运行的
用这种方式判断其实错误的
因为这两个 fmt.Println
并不在同一个栈中,所以没办法判断到底哪个先运行的
你可能会想到,go
能不能 return
一个立即执行函数呢?
非常不好意思,go
不支持 return
一个立即执行函数
那有什么方法可以测试 defer
先执行还是 return
先执行呢?
我们可以通过打印时间戳的方法来判断 return
先执行还是 defer
先执行
因为在编译时,就已经确定了返回值,我们就可以通过这种方式来判断谁先执行的
func main() {
fmt.Println(fn())
}
func fn() string {
defer func() {
now := time.Now()
fmt.Printf("defer : %s\n", now.Format("2006-01-02 15:04:05.999999999"))
}()
now := time.Now()
return fmt.Sprintf("return: %s", now.Format("2006-01-02 15:04:05.999999999"))
}
// defer : 2024-08-10 22:01:03.748488049
// return: 2024-08-10 22:01:03.748433719
从运行结果可以看出,defer
输出的时间晚于 return
的时间
defer 估值时刻
函数 fn
和 fn2
的输出结果是啥?
func fn() {
a := 1
defer fmt.Println(a)
a = 2
}
func fn2() {
a := 1
defer func() { fmt.Println(a) }()
a = 2
}
fn
函数的输出结果是 1
,fn2
函数的输出结果是 2
因为 defer
后面的函数在编译阶段就已经确定了,不会改变,所以 fn
函数会输出 1
但是如果 defer
后面是一个闭包函数,那么这个闭包函数在执行时才能确定内部 a
的变量,所以 fn2
会输出 2
defer 调用 receive 函数输出结果
下面两个 defer
语句的输出结果是啥?
type number int
func (n number) print() {
fmt.Println("print: ", n)
}
func (n *number) printptr() {
fmt.Println("printptr: ", *n)
}
func main() {
var num number
defer num.print()
defer num.printptr()
num = 3
}
defer
是倒序执行,先声明的后执行,所以 n.printptr()
先执行,输出 3
,n.print()
后执行,输出 0
是不是很神奇?
n.printptr
输出 3
比较好理解,因为这是 printptr
方法的接收者是指针类型,所以 n
会拿到最新的值
n.print
虽然是值类型接收器,不是说 go
会隐式实现 func (n *number) print()
方法吗?那为什么拿到的还是 0
呢?
因为 func (n number) print()
,go
确实会为类型隐式地实现 func (n *number) print()
这个方法。但这并不意味着 number
类型会获得一个可以解引用的指针方法,它只是意味着可以用 *number
类型的变量调用 print()
方法,也就是说 var num number
和 var num *number = new(number)
都可以调用 print()
方法`
defer 不推荐使用的场景
- 在循环中使用
defer
,会导致defer
语句在循环结束时才执行,可能会导致资源泄露
func main() {
for i := 0; i < 1000000; i++ {
defer fmt.Println("deferred", i)
}
}
改进,将 for
循环中的 defer
语句放到一个函数中
func main() {
for i := 0; i < 1000000; i++ {
func() {
defer fmt.Println("deferred", i)
}()
}
}
- 不要在
defer
中使用大对象或者大的变量,可能会导致内存泄露
func main() {
data := make([]byte, 102*1024*1024)
defer func() {
fmt.Println("defer", data)
}()
}
改进,显示的释放资源
func main() {
data := make([]byte, 102*1024*1024)
defer func() {
fmt.Println("defer", data)
data = nil
}()
}
defer 结构体
runtime.deferproc
函数用于将一个函数和其他参数封装成一个_defer
结构体,并插入当前协程的defer
栈中runtime.deferreturn
函数用于在函数返回时执行当前defer
栈中的_defer
结构体runtime.deferredFunc
函数用于执行_defer
结构体中封装的函数,并且在执行中捕获panic
type _defer struct {
// 参数和返回值的大小
siz int32
// 表示该 defer 语句是否已经开始执行
started bool
// 表示盖 _defer 语句的优先级
// 当一个 _defer 语句被执行时,它会被添加到 _defer 语句中,而 heap 字段则用于将 _defer 语句添加到一个优先队列中
// 以便在函数返回时按照一定的顺序执行 _defer 语句。在 _defer 链表中,后添加的 _defer 语句会被先执行,而在优先队列中
// heap 值较小的 _defer 语句会被先执行。这个字段的值是在 _defer 语句被添加到 _defer 链表是柑橘一定规则计算出来的
// 通常是根据 _defer 语句的执行顺序和做用户等因素计算而得。在函数返回时,go 语言会按照 heap 值的大小顺序执行 z_defer 语句
// 如果多个 _defer 语句的 heap 值相同,那么它们会按照添加的顺序执行
// 这个机制可以确保 _defer 语句按照一定的顺序执行,从而避免了一些潜在的问题
heap bool
// 表示该 _defer 用于具有开放式编码 _defer 的帧。开放式编码 _defer 是指在编译时已经确定 _defer 语句的数量和位置
// 而不是在运行时动态添加 _defer 语句。在一个帧中,可能会有多个 _defer 语句,但只会有一个 _defer 结构体记录了所有 _defer 语句的信息
// 而 openDefer 就是用来表示该 _defer 结构体是否是针对开放式编码 _defer 的
openDefer bool
// _defer 语句所在栈帧的栈指针(stack pointer)
// 在函数调用时,每个函数都会创建一个新的栈帧,用于保存函数的局部变量,参数和返回值等信息
// 而 _defer 语句也被保存在这个栈帧中,因此需要记录栈指针以便在函数返回时找到 _defer 语句
// 当一个 _defer 语句被执行时,它会被添加到 _defer 链表中,并记录当前栈帧的栈指针
// 在函数返回时,go 语言会遍历 _defer 链表,并执行其中的 _defer 语句,而在执行 _defer 语句时
// 需要之用保存在 _defer 结构体中的栈指针来访问 _defer 语句所在栈帧中的局部变量和参数等信息
// 需要注意的是,由于 _defer 语句是在函数返回之前执行的,因此在执行 _defer 语句时,函数的栈帧可能已经被销毁
// 因此,sp 字段的值不能直接使用,需要通过一些额外的处理来确保 _defer 语句能够正确地访问栈帧中的信息
sp uintptr
// _defer 语句的程序计数器(program counter)
// 程序计数器是一个指针,指向正在执行的函数中的下一条指令,在 _defer 语句被执行时,它会被添加 _defer 链表中
// 并记录当前函数的程序计数器,当函数返回时,go 语言会遍历 _defer 链表,并执行其中的 _defer 语句
// 而在执行 _defer 语句时,需要让程序计数器指向 _defer 语句中的函数调用,以便正确地执行 _defer 语句中的代码
// 这就是为什么 _defer 语句需要记录程序计数器的原因。需要注意的是,由于 _defer 语句是在函数返回之前执行的
// 因此在执行 _defer 语句时,程序计数器可能已经指向了其他的函数或代码块。因此,在执行 _defer 语句时
// 需要使用保存在 _defer 结构体中的程序计数器来确保 _defer 语句中的代码能够正确的执行
pc uintptr // pc 计数器,程序计数器
// _defer 语句的函数地址,也就是验收执行的函数
fn *funcval
// _defer 的 panic 结构体
_panic *_panic
// 用于将多个 defer 链接起来,形成一个 defer 栈
// 当程序执行到一个 defer 语句时,会将该 defer 语句封装成一个 _defer 结构体,并将其插入到 defer 栈的顶部
// 当函数返回时,程序会从 defer 栈的顶部开始依此执行每个 defer 语句,直到 defer 栈为空为止
// 每当 _defer 结构体中的 link 字段指向下一个 _defer 结构体,从而将多个 _defer 结构体链接在一起
// 当程序执行完一个 defer 语句后,会将该 defer 语句从 defer 中弹出,并将其 link 字段指向下一个 _defer 结构体,设置为当前的 defer 栈顶
// 这样,当函数返回时,程序会依此执行每个 defer 语句,从而实现 defer 语句的反转执行顺序的效果
// 需要注意的是,由于 _defer 结构体实在运行时动态创建的,因此 defer 栈的大小是不固定的
// 在编写程序时,应该避免在单个函数中使用大量的 defer 语句,以免导致 defer 栈溢出
link *_defer
}
转载自:https://juejin.cn/post/7401327691893243930