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先入後出的效果。由於本人才疏學淺,如有錯誤請批評指正。