【跟着咱們學Golang】之異常處理

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

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

error接口

Go處理錯誤的思想git

經過返回error接口的方式來處理函數的錯誤,在調用以後進行錯誤的檢查。若是調用該函數出現錯誤,就返回error接口的實現,指出錯誤的具體內容,若是成功,則返回nil做爲error接口的實現。github

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的判斷,着實會讓人感到很是的苦惱,可是它的做用是很好的,其魯棒性也要比其餘靜態語言要好的多。ide

自定義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接口類型,這也舉例說明了上節講到的接口的內容)下面看例子。ui

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'獲取。

關注咱們的「微信公衆號」


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

版權歸做者全部,任何形式轉載請聯繫做者。

做者:搜雲庫技術團隊

出處:https://gostack.souyunku.com/...

相關文章
相關標籤/搜索