天天看點

Go-關鍵字defer、panic、recover詳解deferpanicrecover總結全部源代碼參考

目錄

defer

調用時機

多次調用時的執行順序

傳參問題

源代碼

panic

執行defer

跨協程問題

源代碼

recover

使用

源代碼

總結

defer

panic

recover

全部源代碼

參考

本文進行了關鍵字defer和内建函數panic、recover的介紹和使用細節。

defer

Go 語言的

defer

會在目前函數傳回前執行傳入的函數,它會經常被用于關閉檔案描述符、關閉資料庫連接配接以及解鎖資源。

在文章Go-函數詳解(參數、傳回值、init函數、匿名函數、defer)中進行了簡單的使用,但是還不夠深入,于是在知乎提了問題,今天做下總結。

調用時機

代碼

//------調用時機:所在函數結束或傳回前------
func callTime()  {
	{
		defer fmt.Println("defer in callTime()")
		fmt.Println("code block finish..")
	}
	fmt.Println("callTime() finish...")
}
           

結果

code block finish..

callTime() finish...

defer in callTime()

調用時機在函數/方法結束或傳回前

多次調用時的執行順序

代碼

func moreDefer()  {
	for i:=1;i<5;i++{
		defer fmt.Println("defer",i)
	}
	fmt.Println("moreDefer() finish...")
}
           

結果

moreDefer() finish...

defer 4

defer 3

defer 2

defer 1

棧的順序調用,先入後出。

傳參問題

代碼

func deferPara()  {
	i := 0
	defer fmt.Println("defer",i,"in deferPara()")
	i++
	fmt.Println("deferPara finish...,i is ", i)
}
           

或許你認為結果是這樣的:

deferPara finish...,i is  1

defer 1 in deferPara()

因為defer在函數結束前運作嘛,但事實上結果是這樣的:

deferPara finish...,i is  1
defer 0 in deferPara()
           

defer會在到達所在行時,就将變量複制一份傳過去。想到的解決方案如下:

  • 引用類型就沒有問題了
  • 如果參數是值類型,你的defer不修改參數,你可以傳位址。
  • 如果參數是值類型,你也可以将defer放在函數/方法不修改參數後。
  • 如果參數是值類型,你可以使用匿名函數,函數體内再用參數。
func paraFix()  {
	i := 0
	defer fmt.Println("send addr:defer",&i,"in paraFix()")
	defer func() {fmt.Println("no name func: defer",i,"in paraFix()")}()
	i++
	defer fmt.Println("put defer later:defer",i,"in deferPara()")
}
           

源代碼

結構體

 src->runtime->runtime2.go

type _defer struct {
	siz     int32 // 包含參數和結果
	started bool  // 是否開始
	heap    bool  // 是否配置設定在堆上
	openDefer bool // 是否開放編碼
	sp        uintptr  // 棧指針
	pc        uintptr  // 程式計數器
	fn        *funcval // 開放編碼時可為nil
	_panic    *_panic  // 正在運作的defer的panic
	link      *_defer  // _defer指針
	fd   unsafe.Pointer // 預配置設定的函數資料
	varp uintptr        
	framepc uintptr
}
           

_defer是一個單連結清單(鍊棧),采用頭插的方式,取的時候先取頭的。

編譯

src->cmd->compoile->internal->gc->ssa.go stmt方法的一個case

case ODEFER:
		if Debug_defer > 0 {
			var defertype string
			if s.hasOpenDefers {
				defertype = "open-coded"
			} else if n.Esc == EscNever {
				defertype = "stack-allocated"
			} else {
				defertype = "heap-allocated"
			}
			Warnl(n.Pos, "%s defer", defertype)
		}
		if s.hasOpenDefers {
			s.openDeferRecord(n.Left)
		} else {
			d := callDefer
			if n.Esc == EscNever {
				d = callDeferStack
			}
			s.callResult(n.Left, d)
		}
           

有些defer将在棧上配置設定,有些在堆上配置設定。首先是開放編碼進行優化,其次是棧,最後是堆,配置設定到棧上可以節約記憶體配置設定帶來的額外開銷。

panic

執行defer

當panic異常發生時,程式會中斷運作,并立即執行在該goroutine中被延遲的函數(defer機制)。随後,程式崩潰并輸出日志資訊。

func panicDefer()  {
	fmt.Println("code before panic")
	defer fmt.Println("defer in panicDefer")
	panic("something wrong in panic Defer")
	fmt.Println("code after panic")
}
           

結果

code before panic

defer in panicDefer

panic: something wrong in panic Defer

goroutine 1 [running]:

main.panicDefer()

        E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:42 +0x10a

main.main()

        E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:62 +0x27

exit status 2

利用defer就可以實作有panic時也能進行資源釋放等。

跨協程問題

panic

隻會觸發目前 goroutine 的

defer

func panicGoroutine()  {
	defer println("defer in main")
	go func() {
		defer println("defer in goroutine")
		panic("something wrong...")
	}()

	time.Sleep(time.Second)
}
           

結果:

defer in goroutine

panic: something wrong...

goroutine 6 [running]:

main.panicGoroutine.func1()

        E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:50 +0x78

created by main.panicGoroutine

        E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:48 +0x78

exit status 2

協程外面的defer執行不了

源代碼

結構體

src->runtime->runtime2.go

type _panic struct {
	argp      unsafe.Pointer // 指向defer棧的函數指針
	arg       interface{}    // panic參數
	link      *_panic        // 先前panic的指針
	pc        uintptr        // 運作時,此panic被繞過時傳回到哪
	sp        unsafe.Pointer // 運作時,此panic被繞過時傳回到哪
	recovered bool           // 是否此panic結束
	aborted   bool           // 這個panic被終止
	goexit    bool
}
           

崩潰

src->runtime->panic.go

可以檢視gopanic、fatalpanic兩個函數,代碼過多,不黏貼了,有興趣可以看看。下面是fatalpanic的部分代碼:
           
systemstack(func() {
		exit(2)
	})
           

這就能看出前面panic時為什麼是“exit status 2”了

recover

使用

recover

可以中止

panic

造成的程式崩潰,隻能在

defer

中發揮作用

代碼

func recoverDefer()  {
	//defer println("defer in main") // 執行不到
	defer func() {
		if err := recover();err!=nil{
			println("defer in main")
			println(err)
		}
	}()
	go func() {
		defer println("defer in goroutine")
		panic("something wrong...")
	}()
	panic("something wrong in recoverDefer()")
	time.Sleep(time.Second)
}
           

結果

defer in main

(0xf4d940,0xf85bc8)

defer in goroutine

源代碼

src->runtime->panic.go

func gorecover(argp uintptr) interface{} {
	gp := getg()
	p := gp._panic
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
		p.recovered = true
		return p.arg
	}
	return nil
}
           

可以看到,p!=nil的話才行,也就是說你在panic前使用recover,沒在defer中,那麼傳回的是nil,也就是recover失效了。如果不是nil,在gopanic中會進行處理。

總結

defer

  • 調用時機:函數或方法在傳回或結束前執行
  • 多次調用:先寫的後調用,棧的順序
  • 傳參問題:值類型時,參數不改變後使用defer、defer的函數不修改則傳位址、匿名函數函數體中使用

優點:

  • panic後執行defer,防止異常時忘記釋放資源
  • 函數複雜分支傳回,寫一次即可,簡潔,複用性好

panic

  • panic後執行本協程的defer
  • 跨協程問題使用recover解決

recover

  • 終止panic造成的崩潰
  • 在defer中使用時才有效

全部源代碼

package main

import (
	"fmt"
	"time"
)

//------調用時機:所在函數結束或傳回前------
func callTime()  {
	{
		defer fmt.Println("defer in callTime()")
		fmt.Println("code block finish..")
	}
	fmt.Println("callTime() finish...")
}
//-----多個defer的順序------
func moreDefer()  {
	for i:=1;i<5;i++{
		defer fmt.Println("defer",i)
	}
	fmt.Println("moreDefer() finish...")
}
//-----defer傳參問題-----
func deferPara()  {
	i := 0
	defer fmt.Println("defer",i,"in deferPara()")
	i++
	fmt.Println("deferPara finish...,i is ", i)
}
//-----defer傳參修複方案--------
func paraFix()  {
	i := 0
	defer fmt.Println("send addr:defer",&i,"in paraFix()")
	defer func() {fmt.Println("no name func: defer",i,"in paraFix()")}()
	i++
	defer fmt.Println("put defer later:defer",i,"in deferPara()")
}
//----panic後執行defer-------
func panicDefer()  {
	fmt.Println("code before panic")
	defer fmt.Println("defer in panicDefer")
	panic("something wrong in panic Defer")
	fmt.Println("code after panic")
}
//----跨協程 defer panic問題-----
func panicGoroutine()  {
	defer println("defer in main")
	go func() {
		defer println("defer in goroutine")
		panic("something wrong...")
	}()

	time.Sleep(time.Second)
}
//----------defer中使用recover---------
func recoverDefer()  {
	//defer println("defer in main") // 執行不到
	defer func() {
		if err := recover();err!=nil{
			println("defer in main")
			println(err)
		}
	}()
	go func() {
		defer println("defer in goroutine")
		panic("something wrong...")
	}()
	panic("something wrong in recoverDefer()")
	time.Sleep(time.Second)
}

func main() {
	//callTime()
	//moreDefer()
	//deferPara()
	//paraFix()
	//panicDefer()
	//panicGoroutine()
	recoverDefer()
}
           

參考

知乎-在go語言中,為什麼使用defer?

go-1.16.3源代碼

更多Go相關内容:Go-Golang學習總結筆記

有問題請下方評論,轉載請注明出處,并附有原文連結,謝謝!如有侵權,請及時聯系。如果您感覺有所收獲,自願打賞,可選擇支付寶18833895206(小于),您的支援是我不斷更新的動力。