一、程序設(shè)計(jì)中的錯(cuò)誤和異常處理
對(duì)錯(cuò)誤和異常處理的支持是現(xiàn)代編程語言的標(biāo)配,在Go語言中,程序的錯(cuò)誤由內(nèi)建的error接口支持,errors標(biāo)準(zhǔn)包提供了最基本的錯(cuò)誤處理方法,用戶還可自定義錯(cuò)誤處理。而程序的異常處理通常由panic和recover實(shí)現(xiàn)觸發(fā)和終止。
如果你接觸過類似JAVA、python等語言,你一定了解try...catch...結(jié)構(gòu),但是Go中是沒有try...catch...的,相反你會(huì)看到程序中充斥著大量的if err != nil { ...}這樣的結(jié)構(gòu),這種錯(cuò)誤處理方式和Go的設(shè)計(jì)理念有關(guān),在此之前我們先明確什么是錯(cuò)誤?很么是異常?
- 什么是錯(cuò)誤?
錯(cuò)誤是程序中可能出現(xiàn)的問題,比如連接數(shù)據(jù)庫失敗,連接網(wǎng)絡(luò)失敗等,在程序設(shè)計(jì)中,錯(cuò)誤處理是業(yè)務(wù)的一部分。
Go內(nèi)建一個(gè)error接口類型作為go的錯(cuò)誤標(biāo)準(zhǔn)處理
//go的錯(cuò)誤處理接口
type error interface {
Error() string
}
//errors標(biāo)準(zhǔn)包定義了一個(gè)錯(cuò)誤處理的實(shí)現(xiàn)
// 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
}
- 什么是異常?
異常是指在不該出現(xiàn)問題的地方出現(xiàn)問題,是預(yù)料之外的,比如空指針引用,下標(biāo)越界,向空map添加鍵值等。
Go中內(nèi)建了panic和recover實(shí)現(xiàn)對(duì)異常的觸發(fā)和終止
人為制造一些會(huì)被自動(dòng)觸發(fā)的異常
//1.空指針取值
var aPtr *int //指針聲明本身為空,取不了值
fmt.Println(*aPtr)
//2.下標(biāo)越界
var mm = []int{1, 2, 334, 345, 3}
fmt.Println(mm[6])
//3.向空map添加鍵值對(duì)
var amap map[string]int //只聲明沒初始化的map沒有分配堆內(nèi)存
amap["aaa"] = 111
手工觸發(fā)異常并終止異常
//手工觸發(fā)異常
func triggerPanic(){
panic("這是一個(gè)手工觸發(fā)的恐慌!")
}
//調(diào)用處終止異常轉(zhuǎn)為錯(cuò)誤處理
func main(){
//處理此函數(shù)中調(diào)用其他函數(shù)時(shí)產(chǎn)生的恐慌
defer func() {
if err := recover(); err != nil {
fmt.Println("捕捉一個(gè)恐慌,恐慌信息為:", err)
...
fmt.Println("這里還能繼續(xù)運(yùn)行?。。?!")
}
}()
//調(diào)用一個(gè)會(huì)觸發(fā)恐慌的函數(shù)
triggerPanic()
}
理解了錯(cuò)誤和異常的真正含義,我們就能理解Go的錯(cuò)誤和異常處理的設(shè)計(jì)意圖。傳統(tǒng)的try...catch...結(jié)構(gòu),很容易讓開發(fā)人員把錯(cuò)誤和異?;鞛橐徽?,甚至把業(yè)務(wù)錯(cuò)誤處理的一部分當(dāng)做異常來處理,于是你會(huì)在程序中看到一大堆的catch...。
Go開發(fā)團(tuán)隊(duì)認(rèn)為錯(cuò)誤應(yīng)該明確地當(dāng)成業(yè)務(wù)的一部分,任何可以預(yù)見的問題都需要做錯(cuò)誤處理,于是在Go代碼中,任何調(diào)用者在接收函數(shù)返回值的同時(shí)也需要對(duì)錯(cuò)誤進(jìn)行處理,以防遺漏任何運(yùn)行時(shí)可能的錯(cuò)誤。
異常則是意料之外的,甚至你認(rèn)為在編碼中不可能發(fā)生的,Go遇到異常會(huì)自動(dòng)觸發(fā)panic(恐慌),看這意思就知道,程序的異常是非常嚴(yán)重的,觸發(fā)panic程序會(huì)自動(dòng)退出。除了程序自動(dòng)觸發(fā)異常,一些你認(rèn)為不可允許的情況你也可以手動(dòng)觸發(fā)異常,異常應(yīng)在開發(fā)中盡早的發(fā)現(xiàn)。
然而在Go中除了觸發(fā)異常,還可以終止異常并可選的對(duì)異常進(jìn)行錯(cuò)誤處理,也就是說,錯(cuò)誤和異常是可以相互轉(zhuǎn)換的。
- 錯(cuò)誤轉(zhuǎn)異常:
當(dāng)你在接收到錯(cuò)誤信息,業(yè)務(wù)場(chǎng)景中認(rèn)為該錯(cuò)誤會(huì)影響下一流程時(shí),可選擇手動(dòng)觸發(fā)panic(),異常退出。
- 異常轉(zhuǎn)錯(cuò)誤:
在程序的調(diào)用處,也可以在最根部的main()函數(shù)體內(nèi),在此調(diào)用recover()接收任何被觸發(fā)的異常,如何這些異常可以出來,可以選擇轉(zhuǎn)成錯(cuò)誤處理防止程序退出。
二、Go程序開發(fā)中的錯(cuò)誤處理規(guī)范
1.當(dāng)錯(cuò)誤原因只有一個(gè)時(shí),不返回錯(cuò)誤,直接返回一個(gè)布爾值。
//如一些判斷類的函數(shù)IsXXX(),建議返回布爾值
func IsValid(hostName string) bool {
return hostName == "www.golang.org"
}
2.程序處理中不存在失敗情況時(shí),不返回任何錯(cuò)誤。
//如某些設(shè)置狀態(tài)變量,結(jié)構(gòu)體屬性
var status int
func SetStatus(i int){
status = i
}
3.當(dāng)錯(cuò)誤原因存在多個(gè)且有多個(gè)返回值時(shí),error返回參數(shù)總在最后一個(gè)。
//官方示例
package net/http
func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}
//調(diào)用處
resp, err := http.Get(url)
if err != nil {
return nill, err
}
4.一個(gè)工程化的項(xiàng)目,其錯(cuò)誤原因應(yīng)該統(tǒng)一定義。
//1.最簡(jiǎn)單的方式:使用標(biāo)準(zhǔn)error包預(yù)定義錯(cuò)誤類型
var ERR_NOT_FOUND = errors.New("[error]:File is Not Fount!")
//定義其他類型...
//2.實(shí)現(xiàn)error接口,自定義錯(cuò)誤類型
//自定義錯(cuò)誤結(jié)構(gòu)體
type MyError1 struct {
Code int
Msg string
}
func (me *MyError1) Error() string {
info := fmt.Sprintf("[MyError1]:Code:%d Mes:%s",me.Code,me.Msg)
return info
}
//自定義錯(cuò)誤的工廠方法
func NewMyError1(code int,msg string) *MyError1 {
err := new(MyError1)
err.Code = code
err.Msg = string
return err
}
//定義其他錯(cuò)誤類型...
5.一個(gè)健壯的程序中,調(diào)用者盡可能不要忽略返回的錯(cuò)誤。
//除非你很有信息這錯(cuò)誤不會(huì)影響下面的流程,否則還是建議處理err的好,要謹(jǐn)記一切網(wǎng)絡(luò)都不可信!
resp, _ := http.Get(url)
//類似以上的要少做,一般函數(shù)設(shè)計(jì)中有返回error的,檢查做錯(cuò)誤處理都是必要的!
6.defer處理要在錯(cuò)誤處理之后
//謹(jǐn)記先處理err,否則退出程序
resp, err := http.Get(url)
if err != nil {
return nill, err
}
defer resp.Body.Close()
7.錯(cuò)誤在調(diào)用棧中傳遞時(shí),每層調(diào)用棧都務(wù)必日志記錄,以備溯源。
在開發(fā)過程中,最快的debug方式就是打印日志,在錯(cuò)誤處理的時(shí)候輸出日志,一旦程序報(bào)錯(cuò),通過日志溯源一般都能很快定位,與其在debug時(shí)打斷點(diǎn),日志溯源效率會(huì)高很多
8.有些失敗是偶然性的,如網(wǎng)絡(luò)錯(cuò)誤,可以嘗試一定次數(shù)后再返回失敗。這種錯(cuò)誤處理在網(wǎng)絡(luò)IO的開發(fā)中非常常見。
9.如果業(yè)務(wù)上調(diào)用者不關(guān)心錯(cuò)誤,也可以不必返回錯(cuò)誤。與其說是不關(guān)心,倒不如說關(guān)心也處理不了,比如一些資源清理類的操作,如內(nèi)部出錯(cuò)一般都打印日志即可。
10.出錯(cuò)時(shí),不要輕易忽略除錯(cuò)誤外的其他返回值。一般開發(fā)中我們會(huì)習(xí)慣性的在出錯(cuò)時(shí)把除錯(cuò)誤外的其他返回值置為零值,如果某些計(jì)算結(jié)果外部調(diào)用者需要,則不可忽略。比如,當(dāng)讀取文件發(fā)生錯(cuò)誤時(shí),Read函數(shù)會(huì)返回可以讀取的字節(jié)數(shù)以及錯(cuò)誤信息。對(duì)于這種情況,應(yīng)該將讀取到的字符串和錯(cuò)誤信息一起打印出來。
三、Go程序開發(fā)中的異常處理規(guī)范
1.開發(fā)階段,堅(jiān)持速錯(cuò);
開發(fā)階段盡早發(fā)現(xiàn)錯(cuò)誤,這是常識(shí)。
2.部署階段,終止異常做必要的錯(cuò)誤處理
//某些業(yè)務(wù)場(chǎng)景中會(huì)觸發(fā)panic,但在線上環(huán)境千萬不要暴露丑陋的panic,應(yīng)盡量展示用戶友好的信息提示
func funcA() (err error) {
//A在調(diào)用B時(shí)使用defer延遲接收panic,并把它轉(zhuǎn)為錯(cuò)誤處理
defer func() {
if p := recover(); p != nil {
fmt.Println("panic recover! p:", p)
str, ok := p.(string)
if ok {
err = errors.New(str)
} else {
err = errors.New("panic")
}
//打印輸出一些用戶友好的信息
debug.PrintStack()
}
}()
return funcB()
}
func funcB() error {
// B的業(yè)務(wù)處理流程中有一些不可處理的分支,手工觸發(fā)panic
panic("foo")
}
3.不應(yīng)該出現(xiàn)的情況,手動(dòng)觸發(fā)異常
switch s := suit(drawCard()); s {
case "Spades":
// ...
case "Hearts":
// ...
case "Diamonds":
// ...
case "Clubs":
// ...
default:
panic(fmt.Sprintf("invalid suit %v", s))
}