defer
defer是golang中使用的延迟调用的函数,该函数的使用场景就是如果函数执行出错(panic),也能够通过recover方式进行捕捉错误并将出错时的一些资源进行回收,如果在性能有要求的情况,并且错误能够控制的情况下还是直接避免使用该函数。
defer的使用场景描述
最理想情况下defer的性能对比
package main
import (
"testing"
)
func test_defer(){
defer func(){}()
}
func test_normal(){
func(){}()
}
func BenchmarkNoDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
test_normal()
}
}
func BenchmarkDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
test_defer()
}
}
对该段代码进行基准测试;
go test -bench=. deferw_test.go
goos: darwin
goarch: amd64
BenchmarkNoDefer-4 2000000000 1.21 ns/op
BenchmarkDefer-4 30000000 40.4 ns/op
PASS
ok command-line-arguments 3.797s
发现如果该函数什么也不做的话,调用defer的情况比不调用的情况会有性能上的差距较大。
常用的文件操作defer基准测试
package main
import (
"fmt"
"os"
"testing"
)
func test_defer(){
file, err := os.Open("./test.txt")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
}
func test_normal(){
file, err := os.Open("./test.txt")
if err != nil {
fmt.Println(err)
return
}
file.Close()
}
func BenchmarkNoDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
test_normal()
}
}
func BenchmarkDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
test_defer()
}
}
执行基准测试;
go test -bench=. deferw_test.go
goos: darwin
goarch: amd64
BenchmarkNoDefer-4 100000 17422 ns/op
BenchmarkDefer-4 100000 16553 ns/op
PASS
ok command-line-arguments 3.789s
此时通过基准测试的输出,发现是否使用defer性能几乎相当,在此情况下,其实大部分的性能消耗都位于Open和Close的操作,使得defer在执行的过程中的性能占比很小几乎对整体性能没有影响。
defer捕获panic
package main
import (
"fmt"
)
func test(){
defer func(){
if error := recover(); error != nil {
fmt.Println("error ", error)
}
}()
panic("raise error ")
}
func main() {
test()
fmt.Println("over")
}
此时,输出的结果如下;
error raise error
over
当不加defer中的recover时,此时程序就会报错退出,如果此时还有些需要回收的资源则不能释放,并且recover可以保证所在的协程能够继续运行下去。
defer的使用思考
defer的使用还是需要考虑到是否需要资源的回收,是否需要从异常中恢复或保存信息来做选择,如果在高并发的业务场景下并且当前场景下没有其他的耗时操作则可以考虑选择不用defer,一般在平常的场景下看个人的喜好来选择。
defer的执行过程
为什么在只有defer的操作过程中(本文第一个示例代码),添加了defer的操作性能会小于不添加defer的函数呢?接下来查看一下defer的背后到底做了什么工作。
示例代码
package main
import "fmt"
func main() {
defer func(){
fmt.Println("defer")
}()
}
进行反编译之后获取的指令如下;
deferw.go:6 0x1092ee0 65488b0c2530000000 MOVQ GS:0x30, CX
deferw.go:6 0x1092ee9 483b6110 CMPQ 0x10(CX), SP
deferw.go:6 0x1092eed 764a JBE 0x1092f39
deferw.go:6 0x1092eef 4883ec18 SUBQ $0x18, SP
deferw.go:6 0x1092ef3 48896c2410 MOVQ BP, 0x10(SP)
deferw.go:6 0x1092ef8 488d6c2410 LEAQ 0x10(SP), BP
deferw.go:7 0x1092efd c7042400000000 MOVL $0x0, 0(SP)
deferw.go:7 0x1092f04 488d05cd9d0300 LEAQ go.func.*+125(SB), AX
deferw.go:7 0x1092f0b 4889442408 MOVQ AX, 0x8(SP)
deferw.go:7 0x1092f10 e8eb3bf9ff CALL runtime.deferproc(SB)
deferw.go:7 0x1092f15 85c0 TESTL AX, AX
deferw.go:7 0x1092f17 7510 JNE 0x1092f29
deferw.go:11 0x1092f19 90 NOPL
deferw.go:11 0x1092f1a e87144f9ff CALL runtime.deferreturn(SB)
deferw.go:11 0x1092f1f 488b6c2410 MOVQ 0x10(SP), BP
deferw.go:11 0x1092f24 4883c418 ADDQ $0x18, SP
deferw.go:11 0x1092f28 c3 RET
deferw.go:7 0x1092f29 90 NOPL
deferw.go:7 0x1092f2a e86144f9ff CALL runtime.deferreturn(SB)
deferw.go:7 0x1092f2f 488b6c2410 MOVQ 0x10(SP), BP
deferw.go:7 0x1092f34 4883c418 ADDQ $0x18, SP
deferw.go:7 0x1092f38 c3 RET
deferw.go:6 0x1092f39 e872c2fbff CALL runtime.morestack_noctxt(SB)
deferw.go:6 0x1092f3e eba0 JMP main.main(SB)
从执行的流程可知首先会调用deferproc来创建defer;
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
if getg().m.curg != getg() {
// go code on the system stack can't defer
throw("defer on system stack")
}
// the arguments of fn are in a perilous state. The stack map
// for deferproc does not describe them. So we can't let garbage
// collection or stack copying trigger until we've copied them out
// to somewhere safe. The memmove below does that.
// Until the copy completes, we can only call nosplit routines.
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) // 获取参数的起始地址
callerpc := getcallerpc() // 获取定义的函数的位置
d := newdefer(siz) // 生成一个新的defer结构
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.fn = fn // 设置该接口的执行函数
d.pc = callerpc // 调用的pc地址
d.sp = sp
switch siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
// deferproc returns 0 normally.
// a deferred func that stops a panic
// makes the deferproc return 1.
// the code the compiler generates always
// checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
其中主要就是通过newdefer来创建一个defer,
//go:nosplit
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
gp := getg() // 获取当前的协程
if sc < uintptr(len(p{}.deferpool)) { // 是否小于deferpool
pp := gp.m.p.ptr()
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
// Take the slow path on the system stack so
// we don't grow newdefer's stack.
systemstack(func() {
lock(&sched.deferlock) // 复用defer
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link // 保存d的link
d.link = nil
pp.deferpool[sc] = append(pp.deferpool[sc], d) // 将当前的d 添加到deferpool中
}
unlock(&sched.deferlock)
})
}
if n := len(pp.deferpool[sc]); n > 0 { // 重新初始化deferpool队列
d = pp.deferpool[sc][n-1]
pp.deferpool[sc][n-1] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-1]
}
}
if d == nil { // 如果没有找到则创建一个新的defer
// Allocate new defer+args.
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true)) // 申请内存获取d的空间大小
})
if debugCachedWork {
// Duplicate the tail below so if there's a
// crash in checkPut we can tell if d was just
// allocated or came from the pool.
d.siz = siz
d.link = gp._defer // 将协程的_defer保存到d的link上
gp._defer = d // 设置新的_defer为当前的d
return d
}
}
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
从新建的流程可知,通过协程的_defer来保存该协程中所有的_defer,如果有新增则将新增的添加到头部,通过这么一个链表来完成defer的先入后执行。
当执行到runtime.deferreturn时,就会触发defer的执行。
//go:nosplit
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer // 获取当前协程的_defer链表
if d == nil {
return // 如果为空则返回
}
sp := getcallersp()
if d.sp != sp {
return
}
// Moving arguments around.
//
// Everything called after this point must be recursively
// nosplit because the garbage collector won't know the form
// of the arguments until the jmpdefer can flip the PC over to
// fn.
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn // 获取当前的fn
d.fn = nil // 将当前fn置空
gp._defer = d.link // 获取下一个_defer并设置到_defer中
freedefer(d) // 释放内容
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) // 执行fn函数
}
至此,defer的函数的执行过程就大致执行完成。
总结
defer的使用场景根据需要自行选择,如果在高并发的情况下,还是尽量少使用defer的调用,如果在有出错的情况下且有其他资源需要管理的情况下,建议使用defer来控制资源的回收或释放,并且defer的执行链路都是通过协程来执行的,所以defer执行的过程中要注意是否跨了协程来操作了其他的资源,可能会达不到defer先入后出的效果。由于本人才疏学浅,如有错误请批评指正。