中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

Golang 錯(cuò)誤和異常處理的正確姿勢(shì)

2018-07-20    來(lái)源:編程學(xué)習(xí)網(wǎng)

容器云強(qiáng)勢(shì)上線!快速搭建集群,上萬(wàn)Linux鏡像隨意使用

序言

錯(cuò)誤和異常是兩個(gè)不同的概念,非常容易混淆。很多程序員習(xí)慣將一切非正常情況都看做錯(cuò)誤,而不區(qū)分錯(cuò)誤和異常,即使程序中可能有異常拋出,也將異常及時(shí)捕獲并轉(zhuǎn)換成錯(cuò)誤。從表面上看,一切皆錯(cuò)誤的思路更簡(jiǎn)單,而異常的引入僅僅增加了額外的復(fù)雜度。

但事實(shí)并非如此。眾所周知,Golang遵循“少即是多”的設(shè)計(jì)哲學(xué),追求簡(jiǎn)潔優(yōu)雅,就是說(shuō)如果異常價(jià)值不大,就不會(huì)將異常加入到語(yǔ)言特性中。

錯(cuò)誤和異常處理是程序的重要組成部分,我們先看看下面幾個(gè)問(wèn)題:

  1. 錯(cuò)誤和異常如何區(qū)分?
  2. 錯(cuò)誤處理的方式有哪幾種?
  3. 什么時(shí)候需要使用異常終止程序?
  4. 什么時(shí)候需要捕獲異常?
  5. ...

如果你對(duì)這幾個(gè)問(wèn)題的答案不是太清楚,那么就抽一點(diǎn)時(shí)間看看本文,或許能給你一些啟發(fā)。

face-to-exception.png

基礎(chǔ)知識(shí)

錯(cuò)誤指的是可能出現(xiàn)問(wèn)題的地方出現(xiàn)了問(wèn)題,比如打開(kāi)一個(gè)文件時(shí)失敗,這種情況在人們的意料之中 ;而異常指的是不應(yīng)該出現(xiàn)問(wèn)題的地方出現(xiàn)了問(wèn)題,比如引用了空指針,這種情況在人們的意料之外。可見(jiàn), 錯(cuò)誤是業(yè)務(wù)過(guò)程的一部分,而異常不是

Golang中引入error接口類型作為錯(cuò)誤處理的標(biāo)準(zhǔn)模式,如果函數(shù)要返回錯(cuò)誤,則返回值類型列表中肯定包含error。error處理過(guò)程類似于C語(yǔ)言中的錯(cuò)誤碼,可逐層返回,直到被處理。

Golang中引入兩個(gè)內(nèi)置函數(shù)panic和recover來(lái)觸發(fā)和終止異常處理流程,同時(shí)引入關(guān)鍵字defer來(lái)延遲執(zhí)行defer后面的函數(shù)。

一直等到包含defer語(yǔ)句的函數(shù)執(zhí)行完畢時(shí),延遲函數(shù)(defer后的函數(shù))才會(huì)被執(zhí)行,而不管包含defer語(yǔ)句的函數(shù)是通過(guò)return的正常結(jié)束,還是由于panic導(dǎo)致的異常結(jié)束。你可以在一個(gè)函數(shù)中執(zhí)行多條defer語(yǔ)句,它們的執(zhí)行順序與聲明順序相反。

當(dāng)程序運(yùn)行時(shí),如果遇到引用空指針、下標(biāo)越界或顯式調(diào)用panic函數(shù)等情況,則先觸發(fā)panic函數(shù)的執(zhí)行,然后調(diào)用延遲函數(shù)。調(diào)用者繼續(xù)傳遞panic,因此該過(guò)程一直在調(diào)用棧中重復(fù)發(fā)生:函數(shù)停止執(zhí)行,調(diào)用延遲執(zhí)行函數(shù)等。如果一路在延遲函數(shù)中沒(méi)有recover函數(shù)的調(diào)用,則會(huì)到達(dá)該攜程的起點(diǎn),該攜程結(jié)束,然后終止其他所有攜程,包括主攜程(類似于C語(yǔ)言中的主線程,該攜程ID為1)。

錯(cuò)誤和異常從Golang機(jī)制上講,就是error和panic的區(qū)別。很多其他語(yǔ)言也一樣,比如C++/Java,沒(méi)有error但有errno,沒(méi)有panic但有throw。

Golang錯(cuò)誤和異常是可以互相轉(zhuǎn)換的:

  1. 錯(cuò)誤轉(zhuǎn)異常,比如程序邏輯上嘗試請(qǐng)求某個(gè)URL,最多嘗試三次,嘗試三次的過(guò)程中請(qǐng)求失敗是錯(cuò)誤,嘗試完第三次還不成功的話,失敗就被提升為異常了。
  2. 異常轉(zhuǎn)錯(cuò)誤,比如panic觸發(fā)的異常被recover恢復(fù)后,將返回值中error類型的變量進(jìn)行賦值,以便上層函數(shù)繼續(xù)走錯(cuò)誤處理流程。

一個(gè)啟示

regexp包中有兩個(gè)函數(shù)Compile和MustCompile,它們的聲明如下:

func Compile(expr string) (*Regexp, error)
func MustCompile(str string) *Regexp

同樣的功能,不同的設(shè)計(jì):

  1. Compile函數(shù)基于錯(cuò)誤處理設(shè)計(jì),將正則表達(dá)式編譯成有效的可匹配格式,適用于用戶輸入場(chǎng)景。當(dāng)用戶輸入的正則表達(dá)式不合法時(shí),該函數(shù)會(huì)返回一個(gè)錯(cuò)誤。
  2. MustCompile函數(shù)基于異常處理設(shè)計(jì),適用于硬編碼場(chǎng)景。當(dāng)調(diào)用者明確知道輸入不會(huì)引起函數(shù)錯(cuò)誤時(shí),要求調(diào)用者檢查這個(gè)錯(cuò)誤是不必要和累贅的。我們應(yīng)該假設(shè)函數(shù)的輸入一直合法,當(dāng)調(diào)用者輸入了不應(yīng)該出現(xiàn)的輸入時(shí),就觸發(fā)panic異常。

于是我們得到一個(gè)啟示: 什么情況下用錯(cuò)誤表達(dá),什么情況下用異常表達(dá),就得有一套規(guī)則,否則很容易出現(xiàn)一切皆錯(cuò)誤或一切皆異常的情況。

在這個(gè)啟示下,我們給出異常處理的作用域(場(chǎng)景):

  1. 空指針引用
  2. 下標(biāo)越界
  3. 除數(shù)為0
  4. 不應(yīng)該出現(xiàn)的分支,比如default
  5. 輸入不應(yīng)該引起函數(shù)錯(cuò)誤

其他場(chǎng)景我們使用錯(cuò)誤處理,這使得我們的函數(shù)接口很精煉。對(duì)于異常,我們可以選擇在一個(gè)合適的上游去recover,并打印堆棧信息,使得部署后的程序不會(huì)終止。

說(shuō)明: Golang錯(cuò)誤處理方式一直是很多人詬病的地方,有些人吐槽說(shuō)一半的代碼都是"if err != nil { / 打印 && 錯(cuò)誤處理 / }",嚴(yán)重影響正常的處理邏輯。當(dāng)我們區(qū)分錯(cuò)誤和異常,根據(jù)規(guī)則設(shè)計(jì)函數(shù),就會(huì)大大提高可讀性和可維護(hù)性。

錯(cuò)誤處理的正確姿勢(shì)

姿勢(shì)一:失敗的原因只有一個(gè)時(shí),不使用error

我們看一個(gè)案例:

func (self *AgentContext) CheckHostType(host_type string) error {
    switch host_type {
    case "virtual_machine":
        return nil
    case "bare_metal":
        return nil
    }
    return errors.New("CheckHostType ERROR:" + host_type)
}

我們可以看出,該函數(shù)失敗的原因只有一個(gè),所以返回值的類型應(yīng)該為bool,而不是error,重構(gòu)一下代碼:

func (self *AgentContext) IsValidHostType(hostType string) bool {
    return hostType == "virtual_machine" || hostType == "bare_metal"
}

說(shuō)明:大多數(shù)情況,導(dǎo)致失敗的原因不止一種,尤其是對(duì)I/O操作而言,用戶需要了解更多的錯(cuò)誤信息,這時(shí)的返回值類型不再是簡(jiǎn)單的bool,而是error。

姿勢(shì)二:沒(méi)有失敗時(shí),不使用error

error在Golang中是如此的流行,以至于很多人設(shè)計(jì)函數(shù)時(shí)不管三七二十一都使用error,即使沒(méi)有一個(gè)失敗原因。

我們看一下示例代碼:

func (self *CniParam) setTenantId() error {
    self.TenantId = self.PodNs
    return nil
}

對(duì)于上面的函數(shù)設(shè)計(jì),就會(huì)有下面的調(diào)用代碼:

err := self.setTenantId()
if err != nil {
    // log
    // free resource
    return errors.New(...)
}

根據(jù)我們的正確姿勢(shì),重構(gòu)一下代碼:

func (self *CniParam) setTenantId() {
    self.TenantId = self.PodNs
}

于是調(diào)用代碼變?yōu)椋?/p>

self.setTenantId()

姿勢(shì)三:error應(yīng)放在返回值類型列表的最后

對(duì)于返回值類型error,用來(lái)傳遞錯(cuò)誤信息,在Golang中通常放在最后一個(gè)。

resp, err := http.Get(url)
if err != nil {
    return nill, err
}

bool作為返回值類型時(shí)也一樣。

value, ok := cache.Lookup(key) 
if !ok {
    // ...cache[key] does not exist… 
}

姿勢(shì)四:錯(cuò)誤值統(tǒng)一定義,而不是跟著感覺(jué)走

很多人寫(xiě)代碼時(shí),到處return errors.New(value),而錯(cuò)誤value在表達(dá)同一個(gè)含義時(shí)也可能形式不同,比如“記錄不存在”的錯(cuò)誤value可能為:

  1. "record is not existed."
  2. "record is not exist!"
  3. "###record is not existed。!"
  4. ...

這使得相同的錯(cuò)誤value撒在一大片代碼里,當(dāng)上層函數(shù)要對(duì)特定錯(cuò)誤value進(jìn)行統(tǒng)一處理時(shí),需要漫游所有下層代碼,以保證錯(cuò)誤value統(tǒng)一,不幸的是有時(shí)會(huì)有漏網(wǎng)之魚(yú),而且這種方式嚴(yán)重阻礙了錯(cuò)誤value的重構(gòu)。

于是,我們可以參考C/C++的錯(cuò)誤碼定義文件,在Golang的每個(gè)包中增加一個(gè)錯(cuò)誤對(duì)象定義文件,如下所示:

var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")

說(shuō)明:筆者對(duì)于常量更喜歡C/C++的“全大寫(xiě)+下劃線分割”的命名方式,讀者可以根據(jù)團(tuán)隊(duì)的命名規(guī)范或個(gè)人喜好定制。

姿勢(shì)五:錯(cuò)誤逐層傳遞時(shí),層層都加日志

根據(jù)筆者經(jīng)驗(yàn),層層都加日志非常方便故障定位。

說(shuō)明:至于通過(guò)測(cè)試來(lái)發(fā)現(xiàn)故障,而不是日志,目前很多團(tuán)隊(duì)還很難做到。如果你或你的團(tuán)隊(duì)能做到,那么請(qǐng)忽略這個(gè)姿勢(shì):)

姿勢(shì)六:錯(cuò)誤處理使用defer

我們一般通過(guò)判斷error的值來(lái)處理錯(cuò)誤,如果當(dāng)前操作失敗,需要將本函數(shù)中已經(jīng)create的資源destroy掉,示例代碼如下:

func deferDemo() error {
    err := createResource1()
    if err != nil {
        return ERR_CREATE_RESOURCE1_FAILED
    }
    err = createResource2()
    if err != nil {
        destroyResource1()
        return ERR_CREATE_RESOURCE2_FAILED
    }

    err = createResource3()
    if err != nil {
        destroyResource1()
        destroyResource2()
        return ERR_CREATE_RESOURCE3_FAILED
    }

    err = createResource4()
    if err != nil {
        destroyResource1()
        destroyResource2()
        destroyResource3()
        return ERR_CREATE_RESOURCE4_FAILED
    }
    return nil
}

當(dāng)Golang的代碼執(zhí)行時(shí),如果遇到defer的閉包調(diào)用,則壓入堆棧。當(dāng)函數(shù)返回時(shí),會(huì)按照后進(jìn)先出的順序調(diào)用閉包。

對(duì)于閉包的參數(shù)是值傳遞,而對(duì)于外部變量卻是引用傳遞,所以閉包中的外部變量err的值就變成外部函數(shù)返回時(shí)最新的err值。

根據(jù)這個(gè)結(jié)論,我們重構(gòu)上面的示例代碼:

func deferDemo() error {
    err := createResource1()
    if err != nil {
        return ERR_CREATE_RESOURCE1_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource1()
        }
    }()
    err = createResource2()
    if err != nil {
        return ERR_CREATE_RESOURCE2_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource2()
        }
    }()

    err = createResource3()
    if err != nil {
        return ERR_CREATE_RESOURCE3_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource3()
        }
    }()

    err = createResource4()
    if err != nil {
        return ERR_CREATE_RESOURCE4_FAILED
    }
    return nil
}

姿勢(shì)七:當(dāng)嘗試幾次可以避免失敗時(shí),不要立即返回錯(cuò)誤

如果錯(cuò)誤的發(fā)生是偶然性的,或由不可預(yù)知的問(wèn)題導(dǎo)致。一個(gè)明智的選擇是重新嘗試失敗的操作,有時(shí)第二次或第三次嘗試時(shí)會(huì)成功。在重試時(shí),我們需要限制重試的時(shí)間間隔或重試的次數(shù),防止無(wú)限制的重試。

兩個(gè)案例:

  1. 我們平時(shí)上網(wǎng)時(shí),嘗試請(qǐng)求某個(gè)URL,有時(shí)第一次沒(méi)有響應(yīng),當(dāng)我們?cè)俅嗡⑿聲r(shí),就有了驚喜。
  2. 團(tuán)隊(duì)的一個(gè)QA曾經(jīng)建議當(dāng)Neutron的attach操作失敗時(shí),最好嘗試三次,這在當(dāng)時(shí)的環(huán)境下驗(yàn)證果然是有效的。

姿勢(shì)八:當(dāng)上層函數(shù)不關(guān)心錯(cuò)誤時(shí),建議不返回error

對(duì)于一些資源清理相關(guān)的函數(shù)(destroy/delete/clear),如果子函數(shù)出錯(cuò),打印日志即可,而無(wú)需將錯(cuò)誤進(jìn)一步反饋到上層函數(shù),因?yàn)橐话闱闆r下,上層函數(shù)是不關(guān)心執(zhí)行結(jié)果的,或者即使關(guān)心也無(wú)能為力,于是我們建議將相關(guān)函數(shù)設(shè)計(jì)為不返回error。

姿勢(shì)九:當(dāng)發(fā)生錯(cuò)誤時(shí),不忽略有用的返回值

通常,當(dāng)函數(shù)返回non-nil的error時(shí),其他的返回值是未定義的(undefined),這些未定義的返回值應(yīng)該被忽略。然而,有少部分函數(shù)在發(fā)生錯(cuò)誤時(shí),仍然會(huì)返回一些有用的返回值。比如,當(dāng)讀取文件發(fā)生錯(cuò)誤時(shí),Read函數(shù)會(huì)返回可以讀取的字節(jié)數(shù)以及錯(cuò)誤信息。對(duì)于這種情況,應(yīng)該將讀取到的字符串和錯(cuò)誤信息一起打印出來(lái)。

說(shuō)明:對(duì)函數(shù)的返回值要有清晰的說(shuō)明,以便于其他人使用。

異常處理的正確姿勢(shì)

姿勢(shì)一:在程序開(kāi)發(fā)階段,堅(jiān)持速錯(cuò)

去年學(xué)習(xí)Erlang的時(shí)候,建立了速錯(cuò)的理念,簡(jiǎn)單來(lái)講就是“讓它掛”,只有掛了你才會(huì)第一時(shí)間知道錯(cuò)誤。在早期開(kāi)發(fā)以及任何發(fā)布階段之前,最簡(jiǎn)單的同時(shí)也可能是最好的方法是調(diào)用panic函數(shù)來(lái)中斷程序的執(zhí)行以強(qiáng)制發(fā)生錯(cuò)誤,使得該錯(cuò)誤不會(huì)被忽略,因而能夠被盡快修復(fù)。

姿勢(shì)二:在程序部署后,應(yīng)恢復(fù)異常避免程序終止

在Golang中,雖然有類似Erlang進(jìn)程的Goroutine,但需要強(qiáng)調(diào)的是Erlang的掛,只是Erlang進(jìn)程的異常退出,不會(huì)導(dǎo)致整個(gè)Erlang節(jié)點(diǎn)退出,所以它掛的影響層面比較低,而Goroutine如果panic了,并且沒(méi)有recover,那么整個(gè)Golang進(jìn)程(類似Erlang節(jié)點(diǎn))就會(huì)異常退出。所以,一旦Golang程序部署后,在任何情況下發(fā)生的異常都不應(yīng)該導(dǎo)致程序異常退出,我們?cè)谏蠈雍瘮?shù)中加一個(gè)延遲執(zhí)行的recover調(diào)用來(lái)達(dá)到這個(gè)目的,并且是否進(jìn)行recover需要根據(jù)環(huán)境變量或配置文件來(lái)定,默認(rèn)需要recover。

這個(gè)姿勢(shì)類似于C語(yǔ)言中的斷言,但還是有區(qū)別:一般在Release版本中,斷言被定義為空而失效,但需要有if校驗(yàn)存在進(jìn)行異常保護(hù),盡管契約式設(shè)計(jì)中不建議這樣做。在Golang中,recover完全可以終止異常展開(kāi)過(guò)程,省時(shí)省力。

我們?cè)谡{(diào)用recover的延遲函數(shù)中以最合理的方式響應(yīng)該異常:

  1. 打印堆棧的異常調(diào)用信息和關(guān)鍵的業(yè)務(wù)信息,以便這些問(wèn)題保留可見(jiàn);
  2. 將異常轉(zhuǎn)換為錯(cuò)誤,以便調(diào)用者讓程序恢復(fù)到健康狀態(tài)并繼續(xù)安全運(yùn)行。

我們看一個(gè)簡(jiǎn)單的例子:

func funcA() error {
    defer func() {
        if p := recover(); p != nil {
            fmt.Printf("panic recover! p: %v", p)
            debug.PrintStack()
        }
    }()
    return funcB()
}

func funcB() error {
    // simulation
    panic("foo")
    return errors.New("success")
}

func test() {
    err := funcA()
    if err == nil {
        fmt.Printf("err is nil\\n")
    } else {
        fmt.Printf("err is %v\\n", err)
    }
}

我們期望test函數(shù)的輸出是:

err is foo

實(shí)際上test函數(shù)的輸出是:

err is nil

原因是panic異常處理機(jī)制不會(huì)自動(dòng)將錯(cuò)誤信息傳遞給error,所以要在funcA函數(shù)中進(jìn)行顯式的傳遞,代碼如下所示:

func funcA() (err error) {
    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()
}

姿勢(shì)三:對(duì)于不應(yīng)該出現(xiàn)的分支,使用異常處理

當(dāng)某些不應(yīng)該發(fā)生的場(chǎng)景發(fā)生時(shí),我們就應(yīng)該調(diào)用panic函數(shù)來(lái)觸發(fā)異常。比如,當(dāng)程序到達(dá)了某條邏輯上不可能到達(dá)的路徑:

switch s := suit(drawCard()); s {
    case "Spades":
    // ...
    case "Hearts":
    // ...
    case "Diamonds":
    // ... 
    case "Clubs":
    // ...
    default:
        panic(fmt.Sprintf("invalid suit %v", s))
}

姿勢(shì)四:針對(duì)入?yún)⒉粦?yīng)該有問(wèn)題的函數(shù),使用panic設(shè)計(jì)

入?yún)⒉粦?yīng)該有問(wèn)題一般指的是硬編碼,我們先看“一個(gè)啟示”一節(jié)中提到的兩個(gè)函數(shù)(Compile和MustCompile),其中MustCompile函數(shù)是對(duì)Compile函數(shù)的包裝:

func MustCompile(str string) *Regexp {
    regexp, error := Compile(str)
    if error != nil {
        panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
    }
    return regexp
}

所以,對(duì)于同時(shí)支持用戶輸入場(chǎng)景和硬編碼場(chǎng)景的情況,一般支持硬編碼場(chǎng)景的函數(shù)是對(duì)支持用戶輸入場(chǎng)景函數(shù)的包裝。

對(duì)于只支持硬編碼單一場(chǎng)景的情況,函數(shù)設(shè)計(jì)時(shí)直接使用panic,即返回值類型列表中不會(huì)有error,這使得函數(shù)的調(diào)用處理非常方便(沒(méi)有了乏味的"if err != nil {/ 打印 && 錯(cuò)誤處理 /}"代碼塊)。

小結(jié)

本文以Golang為例,闡述了錯(cuò)誤和異常的區(qū)別,并且分享了很多錯(cuò)誤和異常處理的正確姿勢(shì),這些姿勢(shì)可以單獨(dú)使用,也可以組合使用,希望對(duì)大家有一點(diǎn)啟發(fā)。

 

來(lái)自:http://www.jianshu.com/p/f30da01eea97

 

標(biāo)簽: dns 安全 代碼

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請(qǐng)聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請(qǐng)與原作者聯(lián)系。

上一篇:為什么說(shuō)Python是偉大的入門(mén)語(yǔ)言

下一篇:滴滴出行海量數(shù)據(jù)背后的高可用架構(gòu)