Go語言的錯誤處理

序言

錯誤處理在每一個語言中都是一項重要內容。衆所周知,一般寫程序時遇到的分爲異常與錯誤兩種,Golang中也不例外。Golang遵循『少便是多』的設計哲學,錯誤處理也力求簡潔明瞭,在錯誤處理上採用了相似c語言的錯誤處理方案,另外在錯誤以外也有異常的概念,Golang中引入兩個內置函數panic和recover來觸發和終止異常處理流程。程序員

基礎知識

錯誤指的是可能出現問題的地方出現了問題,好比打開一個文件時可能失敗,這種狀況在人們的意料之中 ;而異常指的是不該該出現問題的地方出現了問題,好比引用了空指針,這種狀況在人們的意料以外。可見, 錯誤是業務邏輯的一部分,而異常不是golang

咱們知道在C語言裏面是經過返回-1或者NULL之類的信息來表示錯誤,可是對於使用者來講,不查看相應的API說明文檔,根本搞不清楚這個返回值究竟表明什麼意思,好比返回0是成功仍是失敗?針對這樣狀況Golang中引入error接口類型做爲錯誤處理的標準模式,若是函數要返回錯誤,則返回值類型列表中確定包含error;Golang中引入兩個內置函數panic和recover來觸發和終止異常處理流程,同時引入關鍵字defer來延遲執行defer後面的函數。一直等到包含defer語句的函數執行完畢時,延遲函數(defer後的函數)纔會被執行,而無論包含defer語句的函數是經過return的正常結束,仍是因爲panic致使的異常結束。你能夠在一個函數中執行多條defer語句,它們的執行順序與聲明順序相反。web

程序運行時若出現了空指針引用、數組下標越界等異常狀況,則會觸發Golang中panic函數的執行,程序會中斷運行,並當即執行在該goroutine中被延遲的函數,若是不作捕獲,程序會崩潰。數據庫

錯誤和異常從Golang機制上講,就是error和panic的區別。不少其餘語言也同樣,好比C++/Java,沒有error但有errno,沒有panic但有throw,但panic的適用場景有一些不一樣。因爲panic會引發程序的崩潰,所以panic通常用於嚴重錯誤。編程

錯誤處理

咱們編寫一個簡單的程序,該程序試圖打開一個不存在的文件:json

package main

import (  
    "fmt"
    "os"
)

func main() {  
    f, err := os.Open("/test.txt")
    if err != nil {
        fmt.Println("error:",err)
        return
    }
    fmt.Println(f.Name(), "open successfully")
}

能夠看到咱們的程序調用了os包的Open方法,該方法定義以下:數組

// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

參考註釋能夠知道若是這個方法正常返回的話會返回一個可讀的文件句柄和一個值爲 nil 的錯誤,若是該方法未能成功打開文件會返回一個*PathError類型的錯誤。服務器

若是一個函數 或方法 返回了錯誤,按照Go的慣例,錯誤會做爲最後一個值返回。因而 Open 函數也是將 err 做爲最後一個返回值返回。框架

在Go語言中,處理錯誤時一般都是將返回的錯誤與 nil 比較。nil 值表示了沒有錯誤發生,而非 nil 值表示出現了錯誤。因而有個咱們上面那行代碼:函數

if err != nil {
        fmt.Println("error:",err)
        return
    }

若是你閱讀任何一個Go語言的工程,會發現相似這樣的代碼隨處可見,Go語言就是用這種簡單的形式處理代碼中出現的錯誤。
咱們在playground中執行,發現結果顯示

error: open /test.txt: No such file or directory

能夠發現咱們有效的檢測並處理了程序中打開一個不存在文件所致使的錯誤,在示例中咱們僅僅是輸出該錯誤並返回。

上面提到Open方法出現錯誤會返回一個*PathError類型的錯誤,這個類型具體是什麼狀況呢?別急,咱們先來看一下Go中錯誤具體是怎麼實現的。

error類型

Go中返回的error類型到底是什麼呢?看源碼發現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 有了一個簽名爲 Error() string 的方法。全部實現該接口的類型均可以看成一個錯誤類型。Error() 方法給出了錯誤的描述。
fmt.Println 在打印錯誤時,會在內部調用 Error() string 方法來獲得該錯誤的描述。上一節示例中的錯誤描述就是這樣打印出的。

自定義錯誤類型

如今咱們回到剛纔代碼裏的*PathError類型,首先顯而易見os.Open方法返回的錯誤是一個error類型,故咱們能夠知道PathError類型必定實現了error類型,也就是說實現了Error方法。如今咱們看下具體實現

type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

能夠看到PathError類型實現了Error方法,該方法返回文件操做、路徑及error字符串的拼接返回值。

爲何須要自定義錯誤類型呢,試想一下若是一個錯誤咱們拿到的僅僅是錯誤的字符串描述,那顯然沒法從錯誤中獲取更多的信息或者作一些邏輯相關的校驗,這樣咱們就能夠經過自定義錯誤的結構體,經過實現Error()來使該結構體成爲一個錯誤類型,使用時作一下類型推薦,咱們就能夠從返回的錯誤經過結構體中的一些成員就能夠作邏輯校驗或者錯誤分類等工做。例如:

package main

import (  
    "fmt"
    "os"
)

func main() {  
    f, err := os.Open("/test.txt")
    if err, ok := err.(*os.PathError); ok {
        fmt.Println("File at path", err.Path, "failed to open")
        return
    }
    fmt.Println(f.Name(), "opened successfully")
}

上面代碼中咱們經過將error類型推斷爲實際的PathError類型,就能夠拿到發生錯誤的Op、Path等數據,更有助於實際場景中錯誤的處理。

咱們組如今拉通了一套錯誤類型和錯誤碼規範,以前工程裏寫的時候都是經過在代碼中的controller裏面去根據不一樣狀況去返回,這種處理方法有不少缺點,例以下層僅返回一個error類型,上層怎麼判斷該錯誤是哪一種錯誤,該使用哪一種錯誤碼呢?另外就是程序中靠程序員寫死某個邏輯錯誤碼爲xxxx,使程序缺少穩定性,錯誤碼返回也較爲爲所欲爲,所以我也去自定義了錯誤,具體以下:

var (
    ErrSuccess           = StandardError{0, "成功"}
    ErrUnrecognized      = StandardError{-1, "未知錯誤"}
    ErrAccessForbid      = StandardError{1000, "沒有訪問權限"}
    ErrNamePwdIncorrect  = StandardError{1001, "用戶名或密碼錯誤"}
    ErrAuthExpired       = StandardError{1002, "證書過時"}
    ErrAuthInvalid       = StandardError{1003, "無效簽名"}
    ErrClientInnerError  = StandardError{4000, "客戶端內部錯誤"}
    ErrParamError        = StandardError{4001, "參數錯誤"}
    ErrReqForbidden      = StandardError{4003, "請求被拒絕"}
    ErrPathNotFount      = StandardError{4004, "請求路徑不存在"}
    ErrMethodIncorrect   = StandardError{4005, "請求方法錯誤"}
    ErrTimeout           = StandardError{4006, "服務超時"}
    ErrServerUnavailable = StandardError{5000, "服務不可用"}
    ErrDbQueryError      = StandardError{5001, "數據庫查詢錯誤"}
)

//StandardError 標準錯誤,包含錯誤碼和錯誤信息
type StandardError struct {
    ErrorCode int    `json:"errorCode"`
    ErrorMsg  string `json:"errorMsg"`
}

// Error 實現了 Error接口
func (err StandardError) Error() string {
    return fmt.Sprintf("errorCode: %d, errorMsg %s", err.ErrorCode, err.ErrorMsg)
}

這樣經過直接取StandardError的ErrorCode就能夠知道應該返回的錯誤信息及錯誤碼,調用時候也較爲方便,而且作到了標準化,解決了以前項目中錯誤處理的問題。

斷言錯誤行爲

有時候僅僅斷言自定義錯誤類型可能在某些狀況下不夠方便,能夠經過調用自定義錯誤的方法來獲取更多信息,例如標準庫中的net包中的DNSError

type DNSError struct {
    Err         string // description of the error
    Name        string // name looked for
    Server      string // server used
    IsTimeout   bool   // if true, timed out; not all timeouts set this
    IsTemporary bool   // if true, error is temporary; not all errors set this
}

func (e *DNSError) Timeout() bool { return e.IsTimeout }

func (e *DNSError) Temporary() bool { return e.IsTimeout || e.IsTemporary }

能夠看到不只僅自定義了DNSError的錯誤類型,還爲該錯誤添加了兩個方法用來讓調用者判判定該錯誤是臨時性錯誤,仍是由超時致使的。

package main

import (
    "fmt"
    "net"
)

func main() {
    addr, err := net.LookupHost("gygolang.com")
    if err, ok := err.(*net.DNSError); ok {
        if err.Timeout() {
            fmt.Println("operation timed out")
        } else if err.Temporary() {
            fmt.Println("temporary error")
        } else {
            fmt.Println("generic error: ", err)
        }
        return
    }
    fmt.Println(addr)
}

上述代碼中,咱們試圖獲取 golangbot123.com(無效的域名) 的 ip。而後經過 *net.DNSError 的類型斷言,獲取到了錯誤的底層值。而後用錯誤的行爲檢查了該錯誤是由超時引發的,仍是一個臨時性錯誤。

異常處理

何時使用panic

須要注意的是,你應該儘量地使用錯誤,而不是使用 panic 和 recover。只有當程序不能繼續運行的時候,才應該使用 panic 和 recover 機制。

panic 有兩個合理的用例:

  • 發生了一個不能恢復的錯誤,此時程序不能繼續運行。 一個例子就是 web 服務器沒法綁定所要求的端口。在這種狀況下,就應該使用 panic,由於若是不能綁定端口,啥也作不了。
  • 發生了一個編程上的錯誤。 假如咱們有一個接收指針參數的方法,而其餘人使用 nil 做爲參數調用了它。在這種狀況下,咱們可使用 panic,由於這是一個編程錯誤:用 nil 參數調用了一個只能接收合法指針的方法。

panic

內置的panic函數定義以下

func panic(v interface{})

當程序終止時,會打印傳入 panic 的參數。咱們一塊兒看一個例子加深下對panic的理解

package main

import (  
    "fmt"
)

func fullName(firstName *string, lastName *string) {  
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    firstName := "foo"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

上面的程序很簡單,若是firstName和lastName有任何一個爲空程序便會panic並打印出不一樣的信息,程序輸出以下:

panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0x1042ff98, 0x0)
    /tmp/sandbox038383853/main.go:12 +0x140
main.main()
    /tmp/sandbox038383853/main.go:20 +0x40

出現panic時,程序終止運行,打印出傳入 panic 的參數,接着打印出堆棧跟蹤。程序首先會打印出傳入 panic 函數的信息:

panic: runtime error: last name cannot be nil

而後打印堆棧信息,首先打印堆棧中的第一項

main.fullName(0x1042ff98, 0x0)
    /tmp/sandbox038383853/main.go:12 +0x140

接着打印堆棧中下一項

main.main()
    /tmp/sandbox038383853/main.go:20 +0x40

在這個例子中這一項就是棧頂了,因而結束打印。

發生panic時的延遲函數

當函數發生 panic 時,它會終止運行,在執行完全部的延遲函數後,程序控制返回到該函數的調用方。這樣的過程會一直持續下去,直到當前協程的全部函數都返回退出,而後程序會打印出 panic 信息,接着打印出堆棧跟蹤,最後程序終止

在上面的例子中,咱們沒有延遲調用任何函數。若是有延遲函數,會先調用它,而後程序控制返回到函數調用方。咱們來修改上面的示例,使用一個延遲語句。

package main

import (  
    "fmt"
)

func fullName(firstName *string, lastName *string) {  
    defer fmt.Println("deferred call in fullName")
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    defer fmt.Println("deferred call in main")
    firstName := "foo"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

能夠看到輸出以下:

deferred call in fullName
deferred call in main
panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0x1042ff90, 0x0)
    /tmp/sandbox170416077/main.go:13 +0x220
main.main()
    /tmp/sandbox170416077/main.go:22 +0xc0

程序退出以前先執行了延遲函數。

recover

程序發生panic後會崩潰,recover用於從新得到 panic 協程的控制。內建的recover函數定義以下

func recover() interface{}

只有在延遲函數的內部,調用 recover 纔有用。在延遲函數內調用 recover,能夠取到 panic 的錯誤信息,而且中止 panic 續發事件(Panicking Sequence),程序運行恢復正常。若是在延遲函數的外部調用 recover,就不能中止 panic 續發事件。
咱們來修改一下程序,在發生 panic 以後,使用 recover 來恢復正常的運行。

package main

import (  
    "fmt"
)

func recoverName() {  
    if r := recover(); r!= nil {
        fmt.Println("recovered from ", r)
    }
}

func fullName(firstName *string, lastName *string) {  
    defer recoverName()
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    defer fmt.Println("deferred call in main")
    firstName := "foo"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

當 fullName 發生 panic 時,會調用延遲函數 recoverName(),它使用了 recover() 來中止 panic 續發事件。程序會輸出

recovered from  runtime error: last name cannot be nil
returned normally from main
deferred call in main

當程序發生 panic 時,會調用延遲函數 recoverName,它反過來會調用 recover() 來從新得到 panic 協程的控制。在執行完 recover() 以後,panic 會中止,程序控制返回到調用方(在這裏就是 main 函數),程序在發生 panic 以後,會繼續正常地運行。程序會打印 returned normally from main,以後是 deferred call in main。

運行時panic

運行時錯誤也會致使 panic。這等價於調用了內置函數 panic,其參數由接口類型 runtime.Error 給出。

package main

import (  
    "fmt"
)

func a() {  
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}
func main() {  
    a()
    fmt.Println("normally returned from main")
}

上述代碼是一個典型的數組越界形成的panic,程序輸出以下:

panic: runtime error: index out of range

goroutine 1 [running]:
main.a()
    /tmp/sandbox100501727/main.go:9 +0x20
main.main()
    /tmp/sandbox100501727/main.go:13 +0x20

能夠看到和咱們剛纔手動出發panic沒什麼不一樣,只是會打印運行時錯誤。
那是否能夠恢復一個運行時 panic?固然是能夠的,也跟剛纔恢復panic的方法同樣,在延遲函數中調用recover便可:

ackage main

import (  
    "fmt"
)

func r() {  
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
    }
}

func a() {  
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}

func main() {  
    a()
    fmt.Println("normally returned from main")
}

錯誤與異常的轉化

錯誤與異常有時候能夠進行轉化,

  • 錯誤轉異常,好比程序邏輯上嘗試請求某個URL,最多嘗試三次,嘗試三次的過程當中請求失敗是錯誤,嘗試完第三次還不成功的話,失敗就被提高爲異常了。
  • 異常轉錯誤,好比panic觸發的異常被recover恢復後,將返回值中error類型的變量進行賦值,以便上層函數繼續走錯誤處理流程。

例如咱們工程中使用的Gin框架裏有這麼兩個函數:

// Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) {
    value, exists = c.Keys[key]
    return
}

// MustGet returns the value for the given key if it exists, otherwise it panics.
func (c *Context) MustGet(key string) interface{} {
    if value, exists := c.Get(key); exists {
        return value
    }
    panic("Key \"" + key + "\" does not exist")
}

能夠看到一樣的功能不一樣的設計:

  1. Get函數基於錯誤設計,若是用戶的參數中沒法取到某參數會返回一個bool類型的錯誤提示。
  2. MustGet基於異常設計,若是沒法取到某參數程序會panic,用於強制取到某參數的硬編碼場景。

能夠看到錯誤跟異常能夠進行轉化,具體怎麼轉化要看業務場景來定。

如何正確且優雅地處理錯誤

error應放在返回值類型列表的最後。

以前看到項目裏有錯誤在中間或者第一個返回的,這是很是不符合規範的。

錯誤值統必定義,而不是爲所欲爲的去寫。

參考以前章節咱們組內拉通的錯誤碼和錯誤信息。

不要忽略錯誤

可能有些時候有些程序員犯懶寫了這樣的代碼

foo, _ := getResult(1)

忽略了錯誤,也就不須要進行校驗了,但這是很危險的,一旦某一個錯誤被忽略沒處理極可能形成下面的程序出bug甚至直接panic。

不要去直接校驗錯誤字符串

好比咱們最先的os.Open函數,咱們去校驗錯誤能這樣寫嗎?

if err.Error == "No such file or directory"

這樣顯然不行,代碼很挫,並且字符判斷很不保險,怎麼辦呢?用上文講的自定義錯誤去作。

小結

本文詳述了Go中錯誤與異常的概念及其處理方法,但願對你們能有啓發。

參考資料

https://studygolang.com/artic...

相關文章
相關標籤/搜索