golang異常處理詳解

小熊今天有意外收穫,忍不住給大家分享我愉快的心情!昨天中午下樓取外賣的時候被一個同事認出來了,他問我:“是不是【編程三分鐘】的作者,文章寫的不錯”。

你知道嗎!我當時就是一愣,然後差點感動到哭出來,雖然小熊的號比不上大牛的號,不能隨便發一篇文章都有成千上萬的閱讀量;但是非常開心的是,我還有你們,默默的關注我,愛你們~!

今天想和大家聊聊 golang 的異常處理

異常處理思想

go 語言裏是沒有 try catch 的概念的,因爲 try catch 會消耗更多資源,而且不管從 try 裏面哪個地方跳出來,都是對代碼正常結構的一種破壞。

所以 go 語言的設計思想中主張

  • 如果一個函數可能出現異常,那麼應該把異常作爲返回值,沒有異常就返回 nil
  • 每次調用可能出現異常的函數時,都應該主動進行檢查,並做出反應,這種 if 語句術語叫衛述語句

所以異常應該總是掌握在我們的手上,保證每次操作產生的影響達到最小,保證程序即使部分地方出現問題,也不會影響整個程序的運行,及時的處理異常,這樣就可以減輕上層處理異常的壓力。

同時也不要讓未知的異常使你的程序崩潰。

異常的形式

我們應該讓異常以這樣的形式出現

func Demo() (int, error)

我們應該讓異常以這樣的形式處理(衛述語句)

_,err := errorDemo()
	if err!=nil{
		fmt.Println(err)
		return
	}

自定義異常

比如程序有一個功能爲除法的函數,除數不能爲 0 ,否則程序爲出現異常,我們就要提前判斷除數,如果爲 0 返回一個異常。那他應該這麼寫。

func divisionInt(a, b int) (int, error) {
	if b == 0 {
		return -1, errors.New("除數不能爲0")
	}

	return a / b, nil
}

這個函數應該被這麼調用

a, b := 4, 0
res, err := divisionInt(a, b)
if err != nil {
	fmt.Println(err.Error())
	return
}
fmt.Println(a, "除以", b, "的結果是 ", res)

可以注意到上面的兩個知識點

  • 創建一個異常 errors.New("字符串")
  • 打印異常信息 err.Error()

只要記得這些,你就掌握了自定義異常的基本方法。

但是 errors.New("字符串") 的形式我不建議使用,因爲他不支持字符串格式化功能,所以我一般使用 fmt.Errorf 來做這樣的事情。

err = fmt.Errorf("產生了一個 %v 異常", "喝太多")

詳細的異常信息

上面的異常信息只是簡單的返回了一個字符串而已,想在報錯的時候保留現場,得到更多的異常內容怎麼辦呢?這就要看看 errors 的內部實現了。其實相當簡單。

errors 實現了一個叫 error 的接口,這個接口裏就一個 Error 方法且返回一個 string ,如下

type error interface {
	Error() string
}

只要結構體實現了這個方法就行,源碼的實現方式如下

type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

// 多一個函數當作構造函數
func New(text string) error {
	return &errorString{text}
}

所以我們只要擴充下自定義 error 的結構體字段就行了。

這個自定義異常可以在報錯的時候存儲一些信息,供外部程序使用

type FileError struct {
	Op   string
	Name string
	Path string
}
// 初始化函數
func NewFileError(op string, name string, path string) *FileError {
	return &FileError{Op: op, Name: name, Path: path}
}
// 實現接口
func (f *FileError) Error() string {
	return fmt.Sprintf("路徑爲 %v 的文件 %v,在 %v 操作時出錯", f.Path, f.Name, f.Op)
}

調用

f := NewFileError("讀", "README.md", "/home/how_to_code/README.md")
fmt.Println(f.Error())

輸出

路徑爲 /home/how_to_code/README.md 的文件 README.md,在 讀 操作時出錯

defer

上面說的內容很簡單,在工作裏也是最常用的,下面說一些拓展知識。

Go 中有一種延遲調用語句叫 defer 語句,它在函數返回時纔會被調用,如果有多個 defer 語句那麼它會被逆序執行。

比如下面的例子是在一個函數內的三條語句,他是這麼怎麼執行的呢?

defer fmt.Println("see you next time!")
defer fmt.Println("close all connect")
fmt.Println("hei boy")

輸出如下, 可以看到兩個 defer 在程序的最後才執行,而且是逆序。

hei boy
close all connect
see you next time!

這一節叫異常處理詳解,終歸是圍繞異常處理來講述知識點, defer 延遲調用語句的用處是在程序執行結束,甚至是崩潰後,仍然會被調用的語句,通常會用來執行一些告別操作,比如關閉連接,釋放資源(類似於 c++ 中的析構函數)等操作。

涉及到 defer 的操作

  • 併發時釋放共享資源鎖
  • 延遲釋放文件句柄
  • 延遲關閉 tcp 連接
  • 延遲關閉數據庫連接

這些操作也是非常容易被人忘記的操作,爲了保證不會忘記,建議在函數的一開始就放置 defer 語句。

panic

剛剛有說到 defer 是崩潰後,仍然會被調用的語句,那程序在什麼情況下會崩潰呢?

Go 的類型系統會在編譯時捕獲很多異常,但有些異常只能在運行時檢查,如數組訪問越界、空指針引用等。這些運行時異常會引起 painc 異常(程序直接崩潰退出)。然後在退出的時候調用當前 goroutinedefer 延遲調用語句。

有時候在程序運行缺乏必要的資源的時候應該手動觸發宕機(比如配置文件解析出錯、依賴某種獨有庫但該操作系統沒有的時候)

defer fmt.Println("關閉文件句柄")

panic("人工創建的運行時異常")

報錯如下

報錯示例

panic recover

出現 panic 以後程序會終止運行,所以我們應該在測試階段發現這些問題,然後進行規避,但是如果在程序中產生不可預料的異常(比如在線的web或者rpc服務一般框架層),即使出現問題(一般是遇到不可預料的異常數據)也不應該直接崩潰,應該打印異常日誌,關閉資源,跳過異常數據部分,然後繼續運行下去,不然線上容易出現大面積血崩。

然後再借助運維監控系統對日誌的監控,發送告警給運維、開發人員,進行緊急修復。

語法如下:

func divisionIntRecover(a, b int) (ret int) {
	defer func() {
		if err := recover(); err != nil {
			// 打印異常,關閉資源,退出此函數
			fmt.Println(err)
			ret = -1
		}
	}()

	return a / b
}

調用

var res int
	datas := []struct {
		a int
		b int
	}{
		{2, 0},
		{2, 2},
	}

	for _, v := range datas {
		if res = divisionIntRecover(v.a, v.b); res == -1 {
			continue
		}
		fmt.Println(v.a, "/", v.b, "計算結果爲:", res)
	}

輸出結果

runtime error: integer divide by zero
2 / 2 計算結果爲: 1
  • 調用 panic 後,當前函數從調用點直接退出
  • recover 函數只有在 defer 代碼塊中纔會有效果
  • recover 可以放在最外層函數,做統一異常處理。

這就是 go 異常處理,我所能想到和找到的全部內容了,希望你在工作中用的更順手。

小熊雖然工作忙,文章沒辦法發的那麼頻繁,但是我有時間就寫一點,反覆校對,代碼也反覆測試最後放 github 上,這樣文章的內容會更完整、更有邏輯、更少異常、對讀者對自己都更負責。如果你發現了文章中出現問題,歡迎在評論區和我討論,非常感謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章