您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關Go中defer和panic以及recover的示例分析,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
defer 語句將一個函數放入一個棧中,defer 會在當前函數返回前執行傳入的函數,經常用于關閉文件描述符,數據庫連接,redis連接等,用于清理資源,避免資源浪費。比如下面這個栗子
package main import ( "fmt" "goapp/src/math") func main() { sum:=math.Add(2,3) fmt.Println(sum) defer func() {fmt.Println("i am defer1")}() res := test_defer(); fmt.Println(res) } func test_defer() float64 { defer func() {fmt.Println("i am defer2")}() defer func() {fmt.Println("i am defer3")}() res :=math3.Mod(5,3) return res; }
執行結果是什么呢?
執行 一個加法,打印返回值 5;
2.defer1入棧
3.執行函數test_defer,defer2入棧,defer3入棧,執行函數邏輯,在return 之前 呢,會執行 棧里面的defer,棧嘛,先進后出,和隊列相反,所以依次執行defer3,defer2,然后返回結果
4.main函數收到test_defer的返回值,開始打印結果
5.main函數在結束之前呢,會執行一下本函數內的defer,所以開始執行defer1
那結果是不是這樣執行的呢?我們來看一下結果,毫無相差
image.png
那么此處可能有小伙伴要問一下,defer為什么要設計成棧?
通俗來講一個場景,defer 是做清場工作的,對吧,那么這樣一個場景,一個小偷去倉庫偷東西,干完活了,要擦除腳印對吧,那他不可能從進門的位置開始擦腳印吧,他只能退著擦,先擦最后一步的腳印,而且,很多時候最后一步是基于前面的基礎上的,比如,還是這個小偷,他想偷柜子里面的珠寶,那他是不是得先打開門啊,那小偷做清理工作的時候,不可能先關閉門,在關閉柜子吧。
defer是用來釋放資源的,比如一個操作,先給文件上鎖,然后修改文件。那defer執行的時候應該是先關閉文件,再釋放鎖。如果釋放鎖,再關閉文件,那不是亂套了嗎?從因果關系上說,后分配的資源可能會依賴前面的資源,但是前面的資源肯定不會依賴后面打開的資源。所以倒過來執行關閉 不會產生問題。
那有的人說,我就是讓defer先進先出,不行嗎?允許是允許,但是不提倡。哈哈,是不是感受到了羅翔老師的氣場,請看下面的代碼,如果defer嵌套,那么defer會從外往里執行,剝洋蔥似的,一層一層剝。
package main func main() { defer func() { println("i am defer1") defer func() { println("i am defer2") defer func() { println("i am defer3") }() }() }() panic("i am panic") }
panic往往和recover是成對出現的,與defer也有緊密的聯系,panic能夠改變程序的控制流,調用panic后會立刻停止執行當前函數的剩余代碼,并在當前Goroutine(協程)中遞歸執行調用方的defer
我們看下面一段代碼
package main import "time"func main() { defer func() { println("i am main defer1") }() go func() { defer func() { println("i am goroutine defer2") }() defer func() { println("i am goroutine defer3") }() panic("i am panic") defer func() { println("i am goroutine defer4") }() }() time.Sleep(1 * time.Second) }
從前面的分析我們得知以下結果
defer1 入棧
2.執行goroutine 3.defer2 入棧 4.defer3入棧 5.panic打斷程序執行,依次執行defer3,defer2,panic,而panic 后面的程序不會再運行,并且main里面的defer也不會執行
image.png
我為什么要加time.Sleep 如果不加呢?
從截圖里面看到,如果沒有time.Sleep,協程好像沒有被執行一樣,為什么會這樣呢?因為我們知道,協程不是搶占式的,如果刪除time.Sleep,主goroutine不會放棄對輔助goroutine的控制,但是goroutine 必須放棄控制才能運行另一個goroutine,而time.Sleep就是放棄控制的一種方法。簡單來說,你這個程序 從頭到尾都是被main 主協程占用著,子協程不會主動搶占cpu,那么必須得是主協程主動讓出cpu,讓子協程有機會被cpu輪詢到,子協程才會被執行
協程是go語言最重要的特色之一,那么我們怎么理解協程呢?協程,簡單說就是輕量級的線程,一個協程的大小是2k 左右,這也解釋了為什么go能單機百萬。
go語言里的協程創建很簡單,在go關鍵詞后面加一個函數調用就可以了。代碼舉栗
package main import "time"func main() { println("i am main goroutine") go func() { println("i am goroutine_in_1") go func() { println("i am goroutine_in_2") go func() { println("i am goroutine_in_3") }() }() }() time.Sleep(1*time.Second); println("main goroutine is over") }
image.png
main 函數是怎么運行的呢?其實main函數也是運行在goroutine里面,不過是主協程,上面的栗子我們是嵌套了幾個協程,但是他們中間并沒有什么層級關系,協程只有兩種,子協程和主協程。上面的代碼中,我們讓主協程休息了一秒,等待子協程返回結果。如果不讓主協程休息一秒,即讓出cpu,讓子協程是沒有機會執行的,因為主協程運行結束后,不管子協程是任何狀態,都會全部消亡。
但是在實際使用中,我們要保護好每一個子協程,確保他們安全運行,一個子協程的異常會傳播到主協程,直接導致主協程掛掉,程序崩潰。比如下面這個栗子
package main import "time"func main() { println("i am main goroutine") go func() { println("i am goroutine_in_1") go func() { println("i am goroutine_in_2") go func() { println("i am goroutine_in_3") panic("i am panic") }() }() }() time.Sleep(1*time.Second); println("main goroutine is over") }
最后一句,main goroutine is over沒有打印,程序沒有執行到。 前面我們說到了。不管你是什么樣的程序,遇到panic 我就終止程序往下執行,哪怕是子協程呢!好了,協程先說到這里。我們繼續往下看recover
recover 可以中止panic造成的程序崩潰,它是一個只能在defer中發揮作用的函數。在其他作用域中不會發揮作用。為什么這么說呢?我們看下面這個栗子
package main import "fmt"func main() { defer func() { println("i am main") }() if err := recover();err != nil { fmt.Println(err) } panic("i am panic") }
看一下執行結果
image.png
我們看到,遇到panic,執行了defer,然后執行了panic ,并沒有執行if條件判斷,為什么?recover是捕捉錯誤的,運行到if 還沒有錯誤,捕捉什么?運行到panic 的時候 if 已經執行過了,怎么捕捉?那么可能有人想,我把if放到panic后面不就行了嗎?行嗎?答案是否定的,panic 我們前面已經說過了,甭管你是誰,看見我就得停止。那就回到我們剛才說的,panic 出現,程序停止往下執行,但是程序會循環執行defer啊,那如果我在defer里面捕捉錯誤,是不是就可以解決這個問題了呢。可見go的設計者是用心良苦!到這里有沒有人會問一個問題defer可以嵌套,那么panic能否嵌套呢?當然可以,defer可以容納一切,panic放到defer里面一樣可以嵌套
package main func main() { defer func() { defer func() { defer func() { panic("i am panic3") }() panic("i am panic2") }() panic("i am panic1") }() panic("i am panic") }
為什么會先執行 最后一行panic ,才執行defer呢,這和前面說的遇到panic先執行defer有點出入是吧,但是你這樣看 defer優先于panic優先于defer+panic。
那么現在,我們來寫一個例子,看defer 如何捕捉panic并恢復程序正常執行
package main import "fmt"func main() { havePanic(); println("i will go on ") } func havePanic() { defer func() { if err:=recover();err !=nil { fmt.Println("i get a panic") fmt.Println(err) } }() panic("i am panic"); }
解讀一下上面的程序,執行havePanic ,havePanic的第一個defer入棧,往下執行碰到panic,首先會執行defer,defer里面打印了err信息,并可以做一些其他的處理,比如記錄日志,重試什么的。然后main繼續執行下面的print,看一下執行結果
image.png
go不是號稱百萬協程嗎?那么我們真給它來個百萬協程看一下我的電腦到底能不能hold住
來!寫一段代碼
package main import ( "fmt" "time") func main() { i :=1; for { go func() { for { time.Sleep(time.Second) } }() if i > 1000000 { fmt.Printf("我已經啟動了%d個協程\n",i) }else{ fmt.Printf("當前是第%d個協程\n",i) } i++ } }
截圖看一下我當前的機器狀態
百萬協程掛起之后的截圖
image.png
因為輸出跟不上速度其實最后跑了1842504個協程
說一下跑后感:風扇呼呼的轉了大概三分鐘的樣子 ,我算了一下一個協程大概是2.45kb的樣子
image.png
一個進程內部可以運行多個線程,而每個線程又可以運行很多協程。線程要負責對協程進行調度,保證每個協程都有機會得到執行。當一個協程睡眠時,它要將線程的運行權讓給其它的協程來運行,而不能持續霸占這個線程。同一個線程內部最多只會有一個協程正在運行。
線程的調度是由操作系統負責的,調度算法運行在內核態,而協程的調用是由 Go 語言的運行時負責的,調度算法運行在用戶態。
協程可以簡化為三個狀態,運行態、就緒態和休眠態。同一個線程中最多只會存在一個處于運行態的協程,就緒態的協程是指那些具備了運行能力但是還沒有得到運行機會的協程,它們隨時會被調度到運行態,休眠態的協程還不具備運行能力,它們是在等待某些條件的發生,比如 IO 操作的完成、睡眠時間的結束等。
操作系統對線程的調度是搶占式的,也就是說單個線程的死循環不會影響其它線程的執行,每個線程的連續運行受到時間片的限制。
Go 語言運行時對協程的調度并不是搶占式的。如果單個協程通過死循環霸占了線程的執行權,那這個線程就沒有機會去運行其它協程了,你可以說這個線程假死了。不過一個進程內部往往有多個線程,假死了一個線程沒事,全部假死了才會導致整個進程卡死。
每個線程都會包含多個就緒態的協程形成了一個就緒隊列,如果這個線程因為某個別協程死循環導致假死,那這個隊列上所有的就緒態協程是不是就沒有機會得到運行了呢?Go 語言運行時調度器采用了 work-stealing 算法,當某個線程空閑時,也就是該線程上所有的協程都在休眠(或者一個協程都沒有),它就會去其它線程的就緒隊列上去偷一些協程來運行。也就是說這些線程會主動找活干,在正常情況下,運行時會盡量平均分配工作任務。
默認情況下,Go 運行時會將線程數會被設置為機器 CPU 邏輯核心數。同時它內置的 runtime 包提供了 GOMAXPROCS(n int) 函數允許我們動態調整線程數,注意這個函數名字是全大寫。該函數會返回修改前的線程數,如果參數 n <=0 ,就不會產生修改效果,等價于讀操作。
package main import ( "fmt" "runtime") func main() { fmt.Print(runtime.GOMAXPROCS(0))//獲取默認線程數 8 println("\n") runtime.GOMAXPROCS(10)//設置線程數為10 fmt.Print(runtime.GOMAXPROCS(0))//獲取新線程數 10 }
關于Go中defer和panic以及recover的示例分析就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。