【跟著我們學Golang】之異常處理

搜雲庫技術團隊發表於2019-05-28

Java中的異常分為Error和Exception來處理,這裡也以錯誤和異常兩種,來分別講一講Go的異常處理。

Go 語言沒有類似 Java 或 .NET 中的異常處理機制,雖然可以使用 defer、panic、recover 模擬,但官方並不主張這樣做。Go 語言的設計者認為其他語言的異常機制已被過度使用,上層邏輯需要為函式發生的異常付出太多的資源。同時,如果函式使用者覺得錯誤處理很麻煩而忽略錯誤,那麼程式將在不可預知的時刻崩潰。 Go 語言希望開發者將錯誤處理視為正常開發必須實現的環節,正確地處理每一個可能發生錯誤的函式。同時,Go 語言使用返回值返回錯誤的機制,也能大幅降低編譯器、執行時處理錯誤的複雜度,讓開發者真正地掌握錯誤的處理。 -- 摘自:C語言中文網

error介面

Go處理錯誤的思想

通過返回error介面的方式來處理函式的錯誤,在呼叫之後進行錯誤的檢查。如果呼叫該函式出現錯誤,就返回error介面的實現,指出錯誤的具體內容,如果成功,則返回nil作為error介面的實現。

error介面宣告瞭一個Error() string 的函式,實際使用時使用相應的介面實現,由函式返回error資訊,函式的呼叫之後進行錯誤的判斷從而進行處理。

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
	Error() string
}
複製程式碼

Error() 方法返回錯誤的具體描述,使用者可以通過這個字串知道發生了什麼錯誤。下面看一個例子。

package main

import (
	"errors"
	"fmt"
)

func main() {
	sources := []string{"hello", "world", "souyunku", "gostack"}
	fmt.Println(getN(0, sources))//直接呼叫,會列印兩項內容,字串元素以及error空物件
	fmt.Println(getN(1, sources))
	fmt.Println(getN(2, sources))
	fmt.Println(getN(3, sources))
	
	target, err := getN(4, sources)//將返回結果賦值
	if err != nil {//常見的錯誤處理,如果error不為nil,則進行錯誤處理
		fmt.Println(err)
		return
	}

	fmt.Println(target)
}

//定義函式獲取第N個元素,正常返回元素以及為nil的error,異常返回空元素以及error
func getN(n int, sources []string) (string, error) {
	if n > len(sources)-1 {
		return "", fmt.Errorf("%d, out of index range %d", n, len(sources) - 1)
	}
	return sources[n], nil
}

/*
列印內容:
hello <nil>
world <nil>
souyunku <nil>
gostack <nil>
 4, out of index range 3
*/
複製程式碼

常見的錯誤處理就是在函式呼叫結束之後進行error的判斷,確定是否出現錯誤,如果出現錯誤則進行相應的錯誤處理;沒有錯誤就繼續執行下面的邏輯。

遇到多個函式都帶有error返回的時候,都需要進行error的判斷,著實會讓人感到非常的苦惱,但是它的作用是很好的,其魯棒性也要比其他靜態語言要好的多。

自定義error

身為一個介面,任何定義實現了Error() string函式,都可以認為是error介面的實現。所以可以自己定義具體的介面實現來滿足業務的需求。

error介面的實現有很多,各大專案也都喜歡自己實現error介面供自己使用。最常用的是官方的error包下的errorString實現。

// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package errors implements functions to manipulate errors.
package errors

// New returns an error that formats as the given text.
func New(text string) error {
	return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}
複製程式碼

可以看到,官方error包通過定義了errorString來實現了error介面,在使用的時候通過New(text string) error這個函式進行呼叫從而返回error介面內容(該函式在返回的時候是一個errorString型別的指標,但是定義的返回內容是error介面型別,這也舉例說明了上節講到的介面的內容)下面看例子。

package main

import (
	"errors"
	"fmt"
)

func main() {
	//直接使用errors.New來定義錯誤訊息
	notFound := errors.New("404 not found")
	fmt.Println(notFound)

	//也可以使用fmt包中包裝的Errorf來新增
	fmt.Println(fmt.Errorf("404: page %v is not found","index.html"))
}

/*
列印內容
404 not found
404: page index.html is not found
*/
複製程式碼

自己試著實現一個404notfound的異常

type NOTFoundError struct {
	name string
}

func (e *NOTFoundError) Error() string {
	return fmt.Sprintf("%s  is not found, please new again", e.name)
}

func NewNotFoundError(name string) error{
	return &NOTFoundError{name}
}

func runDIYError() {
	err := NewNotFoundError("your girl")

	// 根據switch,確定是哪種error
	switch err.(type) {
	case *NOTFoundError:
		fmt.Printf("error : %v \n",err)
	default: // 其他型別的錯誤
		fmt.Println("other error")
	}
}

/**呼叫runDIYError()結果
error : your girl  is not found, please new again 
*/
複製程式碼

自己定義異常NotFoundError只是簡單的實現Error() string函式,並在出錯的時候提示內容找不到,不支援太多的功能,如果業務需要,還是可以繼續擴充套件。

defer

在將panic和recover之前插播一下defer這個關鍵字,這個關鍵字在panic和recover中也會用到。

defer的作用就是指定某個函式在執行return之前在執行,而不是立即執行。下面是defer的語法

defer func(){}()
複製程式碼

defer指定要執行的函式,或者直接宣告一個匿名的函式並直接執行。這個還是結合例項進行了解比較合適。


func runDefer(){
	defer func() {
		fmt.Println("3")
	}()//括號表示定義function之後直接執行

	fmt.Println("1")

	defer func(index string) {
		fmt.Println(index)
	}("2")//括號表示定義function之後直接執行,如果定義的function包含引數,括號中也要進行相應的賦值操作
}

/**
執行結果:
1
2
3
*/
複製程式碼

執行該函式能看到順序列印出了123三個數字,這就是defer的執行過程。其特點就是LIFO,先進後出,先指定的函式總是在後面執行,是一個逆序的執行過程。

defer在Go中也是經常被用到的,而且設計的極其巧妙,舉個例子

file.Open()
defer file.Close()//該語句緊跟著file.Open()被指定

file.Lock()
defer file.Unclock()// 該語句緊跟著file.Lock()被指定
複製程式碼

像這樣需要開關或者其他操作必須執行的操作都可以在相鄰的行進行執行指定,可以說很好的解決了那些忘記執行Close操作的痛苦。

defer面試題

 
package main
 
import (
    "fmt"
)
 
func main() {
    defer_call()
}
 
func defer_call() {
    defer func() { fmt.Println("列印前") }()
    defer func() { fmt.Println("列印中") }()
    defer func() { fmt.Println("列印後") }()
 
    panic("觸發異常")
}

考點:defer執行順序
解答:
defer 是後進先出。
panic 需要等defer 結束後才會向上傳遞。 出現panic恐慌時候,會先按照defer的後入先出的順序執行,最後才會執行panic。

結果:
列印後
列印中
列印前
panic: 觸發異常
 --- 
//摘自:https://blog.csdn.net/weiyuefei/article/details/77963810

複製程式碼
func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}
 
func main() {
    a := 1
    b := 2
    defer calc("1", a, calc("10", a, b))
    a = 0
    defer calc("2", a, calc("20", a, b))
    b = 1
}

考點:defer執行順序
解答:
這道題類似第1題 需要注意到defer執行順序和值傳遞 index:1肯定是最後執行的,但是index:1的第三個引數是一個函式,所以最先被呼叫calc("10",1,2)==>10,1,2,3 執行index:2時,與之前一樣,需要先呼叫calc("20",0,2)==>20,0,2,2 執行到b=1時候開始呼叫,index:2==>calc("2",0,2)==>2,0,2,2 最後執行index:1==>calc("1",1,3)==>1,1,3,4

結果:
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4

---
摘自:  https://blog.csdn.net/weiyuefei/article/details/77963810
複製程式碼

defer 雖然是基礎知識,其呼叫過程也非常好理解,但是往往在面試的過程中會出現一些比較繞的題目,這時候不要驚慌,只需要好好思考其執行的過程還是可以解出來的。

panic & recover

panic英文直譯是恐慌,在Go中意為程式出現了崩潰。recover直譯是恢復,其目的就是恢復恐慌。

在其他語言裡,當機往往以異常的形式存在。底層丟擲異常,上層邏輯通過 try/catch 機制捕獲異常,沒有被捕獲的嚴重異常會導致當機,捕獲的異常可以被忽略,讓程式碼繼續執行。 Go 沒有異常系統,其使用 panic 觸發當機類似於其他語言的丟擲異常,那麼 recover 的當機恢復機制就對應 try/catch 機制。-- 摘自:C語言中文網

panic

程式崩潰就像遇到電腦藍屏時一樣,大家都不希望遇到這樣的情況。但有時程式崩潰也能終止一些不可控的情況,以此來做出防範。出於學習的目的,我們們簡單瞭解一下panic造成的崩潰,以及如何處理。先看一下panic的定義。

// The panic built-in function stops normal execution of the current
// goroutine. When a function F calls panic, normal execution of F stops
// immediately. Any functions whose execution was deferred by F are run in
// the usual way, and then F returns to its caller. To the caller G, the
// invocation of F then behaves like a call to panic, terminating G's
// execution and running any deferred functions. This continues until all
// functions in the executing goroutine have stopped, in reverse order. At
// that point, the program is terminated and the error condition is reported,
// including the value of the argument to panic. This termination sequence
// is called panicking and can be controlled by the built-in function
// recover.
func panic(v interface{})
複製程式碼

從定義中可以瞭解到,panic可以接收任何型別的資料。而接收的資料可以通過recover進行獲取,這個後面recover中進行講解。

從分類上來說,panic的觸發可以分為兩類,主動觸發和被動觸發。

在程式執行期間,主動執行panic可以提前中止程式繼續向下執行,避免造成更惡劣的影響。同時還能根據列印的資訊進行問題的定位。

func runSimplePanic(){
	defer func() {
		fmt.Println("before panic")
	}()
	panic("simple panic")
}

/**
呼叫runSimplePanic()函式結果:
before panic
panic: simple panic

goroutine 1 [running]:
main.runSimplePanic()
	/Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:102 +0x55
main.main()
	/Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:18 +0x22
*/
複製程式碼

從執行結果中能看到,panic執行後先執行了defer中定義的函式,再列印的panic的資訊,同時還給出了執行panic的具體行(行數需要針對具體程式碼進行定論),可以方便的進行檢查造成panic的原因。

還有在程式中不可估計的panic,這個可以稱之為被動的panic,往往由於空指標和陣列下標越界等問題造成。

func runBePanic(){
	fmt.Println(ss[100])//ss集合中沒有下標為100的值,會造成panic異常。
}

/**
呼叫runBePanic()函式結果:
panic: runtime error: index out of range

goroutine 1 [running]:
main.runBePanic(...)
	/Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:106
main.main()
	/Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:21 +0x10f
*/
複製程式碼

從執行結果中看到,陣列下標越界,直接導致panic,panic資訊也是有Go系統執行時runtime所提供的資訊。

recover

先來簡單看一下recover的註釋。

// The recover built-in function allows a program to manage behavior of a
// panicking goroutine. Executing a call to recover inside a deferred
// function (but not any function called by it) stops the panicking sequence
// by restoring normal execution and retrieves the error value passed to the
// call of panic. If recover is called outside the deferred function it will
// not stop a panicking sequence. In this case, or when the goroutine is not
// panicking, or if the argument supplied to panic was nil, recover returns
// nil. Thus the return value from recover reports whether the goroutine is
// panicking.
func recover() interface{}
複製程式碼

註釋指明recover可以管理panic,通過defer定義在panic之前的函式中的recover,可以正確的捕獲panic造成的異常。

結合panic來看一下recover捕獲異常,並繼續程式處理的簡單實現。


import "fmt"

func main() {
	runError()

	fmt.Println("---------------------------")
	runPanicError()
}

type Student struct {
	Chinese int
	Math    int
	English int
}

var ss = []Student{{100, 90, 89},
	{80, 80, 80},
	{70, 80, 80},
	{70, 80, 60},
	{90, 80, 59},
	{90, 40, 59},
	{190, 40, 59},
	{80, 75, 66},
}

func runError() {

	i := 0

	for ; i < len(ss); i++ {
		flag, err := checkStudent(&ss[i])
		if err != nil {
			fmt.Println(err)
			return
		}//遇到異常資料就會立即返回,不能處理剩餘的資料
		//而且,正常邏輯中參雜異常處理,使得程式並不是那麼優雅

		fmt.Printf("student %#v,及格? :%t \n", ss[i], flag)
	}

}

func checkStudent(s *Student) (bool, error) {
	if s.Chinese > 100 || s.Math > 100 || s.English > 100 {
		return false, fmt.Errorf("student %#v, something error", s)
	}

	if s.Chinese > 60 && s.Math > 60 && s.English > 60 {
		return true, nil
	}

	return false, nil
}

func runPanicError() {
	i := 0
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
		i ++//跳過異常的資料,繼續處理剩餘的資料
		for ; i < len(ss); i ++ {
			fmt.Printf("student %#v,及格? :%t \n", ss[i], checkStudentS(&ss[i]))
		}
	}()

	for ; i < len(ss); i++ {
		fmt.Printf("student %#v,及格? :%t \n", ss[i], checkStudentS(&ss[i]))
	}

}

func checkStudentS(s *Student) bool {
	if s.Chinese > 100 || s.Math > 100 || s.English > 100 {
		panic(fmt.Errorf("student %#v, something error", s))
	}

	if s.Chinese > 60 && s.Math > 60 && s.English > 60 {
		return true
	}

	return false
}
結果:

student main.Student{Chinese:100, Math:90, English:89},及格? :true 
student main.Student{Chinese:80, Math:80, English:80},及格? :true 
student main.Student{Chinese:70, Math:80, English:80},及格? :true 
student main.Student{Chinese:70, Math:80, English:60},及格? :false 
student main.Student{Chinese:90, Math:80, English:59},及格? :false 
student main.Student{Chinese:90, Math:40, English:59},及格? :false 
student &main.Student{Chinese:190, Math:40, English:59}, something error
---------------------------
student main.Student{Chinese:100, Math:90, English:89},及格? :true 
student main.Student{Chinese:80, Math:80, English:80},及格? :true 
student main.Student{Chinese:70, Math:80, English:80},及格? :true 
student main.Student{Chinese:70, Math:80, English:60},及格? :false 
student main.Student{Chinese:90, Math:80, English:59},及格? :false 
student main.Student{Chinese:90, Math:40, English:59},及格? :false 
student &main.Student{Chinese:190, Math:40, English:59}, something error
student main.Student{Chinese:80, Math:75, English:66},及格? :true 

複製程式碼

從結果中可以看出runPanicError函式將全部正常的資料都輸出了,並給出了是否及格的判斷,runError並沒有全部將資料輸出,而是遇到錯誤就中止了後續的執行,導致了執行的不夠徹底。

panic和recover的用法雖然簡單,但是一般程式中用到的卻很少,除非你對panic有著很深的瞭解。但也可以通過Panic來很好的美化自己的程式碼,從程式上看,runPanicError中的異常處理與正常邏輯區分開,也使得程式看起來非常的舒暢-_-!

相對於那些對panic和recover掌握非常好的人來說,panic和recover能隨便用,真的可以御劍飛行那種;但是如果掌握不好的話,還是儘可能的使用相對簡單但不失高效又能很好的解決問題的error來處理就好了,以此來避免過度的使用從而造成的意外影響。畢竟我們的經驗甚少,複雜的事物還是交給真正的大佬比較合適。

總結

Go中的異常處理相對比Java這些有著相對完善的錯誤處理機制的語言來說,還是顯得非常的低階的,這也是Go一直被大家詬病的一點,但Go的更新計劃中也有針對異常處理的改善,相信用不了多久就能看到不一樣的錯誤處理機制。

原始碼可以通過'github.com/souyunkutech/gosample'獲取。

關注我們的「微信公眾號」

【跟著我們學Golang】之異常處理


首發微信公眾號:Go技術棧,ID:GoStack

版權歸作者所有,任何形式轉載請聯絡作者。

作者:搜雲庫技術團隊

出處:gostack.souyunku.com/2019/05/27/…

相關文章