Golang Recover的一個小坑

1.error

Golang被詬病很是多的一點就是缺乏強大方便的異常處理機制,大部分高級編程語言,好比Java、PHP、Python等都擁有一種try catch機制,這種異常捕獲機制能夠很是方便的處理程序運行中可能出現的各類意外狀況。編程

嚴格來講,在Go裏面,錯誤和異常是2種不一樣的類型,錯誤通常是指程序產生的邏輯錯誤,或者意料之中的意外狀況,並且異常通常就是panic,好比角標越界、段錯誤。編程語言

對於錯誤,Golang採用了一種很是原始的手段,咱們必須手動處理可能產生的每個錯誤,通常會把錯誤返回給調用方,下面這種寫法在Go裏面十分常見:ide

package main
import (
	"errors"
	"fmt"
)

func main() {
	s, err := say()
	if err != nil {
		fmt.Printf("%s\n", err.Error())
	} else {
		fmt.Printf("%s\n", s)
	}
}

func say() (string, error) {
	// do something
	return "", errors.New("something error")
}
複製代碼

這種寫法最大的問題就是每個error都須要判斷處理,很是繁瑣,若是使用try catch機制,咱們就能夠統一針對多個函數調用可能產生的錯誤作處理,節省一點代碼和時間。不過我們今天不是來討論Go的異常錯誤處理機制的,這裏只是簡單說一下。函數

2.panic

通常錯誤都是顯示的,程序明確返回的,而異常每每是隱示的,不可預測的,好比下面的代碼:性能

package main

import "fmt"

func main() {
	fmt.Printf("%d\n", cal(1,2))
	fmt.Printf("%d\n", cal(5,2))
	fmt.Printf("%d\n", cal(5,0)) //panic: runtime error: integer divide by zero 
	fmt.Printf("%d\n", cal(9,5))
}

func cal(a, b int) int {
	return a / b
}
複製代碼

在執行第三個計算的時候會發生一個panic,這種錯誤會致使程序退出,下面的代碼的就沒法執行了。固然你能夠說這種錯誤理論上是能夠預測的,咱們只要在cal函數內部作好處理就好了。ui

然而實際開發中,會發生panic的地方可能特別多,並且不是這種一眼就能看出來的,在Web服務中,這樣的panic會致使整個Web服務掛掉,特別危險。spa

3.recover

雖然沒有try catch機制,Go其實有一種相似的recover機制,功能弱了點,用法很簡單:日誌

package main

import "fmt"

func main() {
	fmt.Printf("%d\n", cal(1, 2))
	fmt.Printf("%d\n", cal(5, 2))
	fmt.Printf("%d\n", cal(5, 0))
	fmt.Printf("%d\n", cal(9, 2))
}

func cal(a, b int) int {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("%s\n", err)
		}
	}()
	return a / b
}
複製代碼

首先,你們得理解defer的做用,簡單說defer就相似於面向對象裏面的析構函數,在這個函數終止的時候會執行,即便是panic致使的終止。code

因此,在cal函數裏面每次終止的時候都會檢查有沒有異常產生,若是產生了咱們能夠處理,好比說記錄日誌,這樣程序還能夠繼續執行下去。協程

4.注意的坑

通常defer recover這種機制常常用在常駐進程的應用,好比Web服務,在Go裏面,每個Web請求都會分配一個goroutine去處理,在沒有作任何處理的狀況下,假如某一個請求發生了panic,就會致使整個服務掛掉,這是不可接受的,因此在Web應用裏面必須使用recover保證即便某一個請求發生錯誤也不影響其它請求。

這裏我使用一小段代碼模擬一下:

package main

import (
	"fmt"
)

func main() {
	requests := []int{12, 2, 3, 41, 5, 6, 1, 12, 3, 4, 2, 31}
	for n := range requests {
		go run(n) //開啓多個協程
	}

	for {
		select {}
	}
}

func run(num int) {
    //模擬請求錯誤
	if num%5 == 0 {
		panic("請求出錯")
	}
	fmt.Printf("%d\n", num)
}
複製代碼

上面這段代碼沒法完整執行下去,由於其中某一個協程必然會發生panic,從而致使整個應用掛掉,其它協程也中止執行。

解決方法和上面同樣,咱們只須要在run函數裏面加入defer recover,整個程序就會很是健壯,即便發生panic,也會完整的執行下去。

func run(num int) {
	defer func() {
		if err := recover();err != nil {
			fmt.Printf("%s\n", err)
		}
	}()
	if num%5 == 0 {
		panic("請求出錯")
	}
	fmt.Printf("%d\n", num)
}
複製代碼

上面的代碼只是演示,真正的坑是:若是你在run函數裏面又啓動了其它協程,這個協程發生的panic是沒法被recover的,仍是會致使整個進程掛掉,咱們改造了一下上面的例子:

func run(num int) {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("%s\n", err)
		}
	}()
	if num%5 == 0 {
		panic("請求出錯")
	}
	go myPrint(num)
}

func myPrint(num int) {
	if num%4 == 0 {
		panic("請求又出錯了")
	}
	fmt.Printf("%d\n", num)
}
複製代碼

我在run函數裏面又經過協程的方式調用了另外一個函數,而這個函數也會發生panic,你會發現整個程序也掛了,即便run函數有recover也沒有任何做用,這意味着咱們還須要在myPrint函數裏面加入recover。可是若是你不使用協程的方式調用myPrint函數,直接調用的話仍是能夠捕獲recover的。

總結一下就是defer recover這種機制只是針對當前函數和以及直接調用的函數可能產生的panic,它沒法處理其調用產生的其它協程的panic,這一點和try catch機制不同。

理論上講,全部使用協程的地方都必須作defer recover處理,這樣才能保證你的應用萬無一失,不過開發中能夠根據實際狀況而定,對於一些不可能出錯的函數加了還影響性能。

Go的Web服務也是同樣,默認的recover機制只能捕獲一層,若是你在這個請求的處理中又使用了其它協程,那麼必須很是慎重,畢竟只要發生一個panic,整個Web服務就會掛掉。

最後,總結一下,Go的異常處理機制雖然沒有不少其它語言高效,可是基本上仍是能知足需求,目前官方已經在着完善這一點,Go2可能會見到。

相關文章
相關標籤/搜索