溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Go36-19,20-錯(cuò)誤處理

發(fā)布時(shí)間:2020-10-17 20:24:12 來(lái)源:網(wǎng)絡(luò) 閱讀:267 作者:騎士救兵 欄目:編程語(yǔ)言

錯(cuò)誤處理(上)

錯(cuò)誤處理到現(xiàn)在為止應(yīng)該已經(jīng)接觸過(guò)幾次了。比如,聲明error類型的變量err,或是調(diào)用errors包中的New函數(shù)。

error類型

error類型是一個(gè)接口類型,是一個(gè)Go語(yǔ)言的內(nèi)建類型。在這個(gè)接口類型的聲明中只包含了一個(gè)方法Error。這個(gè)方法不接受任何參數(shù),但是會(huì)返回一個(gè)string類型的結(jié)果。它的作用是返回錯(cuò)誤信息的字符串表示形式。使用error類型的方式通常是,在函數(shù)聲明的結(jié)果列表的最后,聲明一個(gè)該類型的結(jié)果,同時(shí)在調(diào)用這個(gè)函數(shù)之后,先判斷它返回的最后一個(gè)結(jié)果值是否“不為nil”。如果值“不為nil”,就需要進(jìn)入錯(cuò)誤處理。否則就是繼續(xù)正常的流程。示例如下:

package main

import "fmt"

func echo(request string) (response string, err error) {
    if request == "" {
        err = fmt.Errorf("空字符串")  // 這里底層也是調(diào)用下面的New,但是支持字符串格式化
        // 如果是純字符串,可以直接調(diào)用errors包里的New函數(shù)
        // err = errors.New("empty request")
        return
    }
    response = fmt.Sprintf("echo:%s", request)
    return
}

func main() {
    for _, req := range []string{"", "Hello"} {
        fmt.Printf("request: %s\n", req)
        resp, err := echo(req)
        if err != nil {
            fmt.Printf("error: %s\n", err)
            continue
        }
        fmt.Printf("response: %s\n", resp)
    }
}

在echo函數(shù)和main函數(shù)中,我都使用到了衛(wèi)述語(yǔ)句。衛(wèi)述語(yǔ)句,就是被用來(lái)檢查后續(xù)操作的前置條件并進(jìn)行相應(yīng)處理的語(yǔ)句。在進(jìn)行錯(cuò)誤處理的時(shí)候經(jīng)常會(huì)用到衛(wèi)述語(yǔ)句,以至于“我的程序滿屏都是衛(wèi)述語(yǔ)句,簡(jiǎn)直是太難看了!”(這里我有同感)。

錯(cuò)誤判斷

由于error是一個(gè)接口類型,所以即使同為error類型的錯(cuò)誤值,它們的實(shí)際類型也可能不同。錯(cuò)誤判斷的做法一般是如下的3種:

  1. 對(duì)于類型在已知范圍內(nèi)的一系列錯(cuò)誤值,一般使用類型斷言表達(dá)式或類型switch語(yǔ)句來(lái)判斷
  2. 對(duì)于已有相應(yīng)變量且類型相同的一系列錯(cuò)誤值,一般直接使用判等操作來(lái)判斷
  3. 對(duì)于沒有相應(yīng)變量且類型未知的一系列錯(cuò)誤值,只能使用其錯(cuò)誤信息的字符串表示形式來(lái)做判斷

對(duì)于上面的3種情況,接下來(lái)分別展開。

第一種情況
類型在已知范圍內(nèi)的錯(cuò)誤值是最容易分辨的。拿os包中的幾個(gè)代表錯(cuò)誤的類型os.PathError、os.LinkError、os.SyscallError和os/exec.Error舉例,它們的指針類型都是error接口的實(shí)現(xiàn)類型,同時(shí)它們也都包含了一個(gè)名叫Err,類型為error接口類型的代表潛在錯(cuò)誤的字段。
如果得到一個(gè)error類型值,并且知道該值的實(shí)際類型肯定是它們中的某一個(gè),那就可以用類型switch語(yǔ)句去做判斷。示例如下:

package main

import (
    "fmt"
    "os"
    "os/exec"
)

// underlyingError 會(huì)返回已知的操作系統(tǒng)相關(guān)錯(cuò)誤的潛在錯(cuò)誤值。
func underlyingError(err error) error {
    switch err := err.(type) {
    case *os.PathError:
        return err.Err
    case *os.LinkError:
        return err.Err
    case *os.SyscallError:
        return err.Err
    case *exec.Error:
        return err.Err
    }
    return err
}

func main() {
    r, w, err := os.Pipe()
    if err != nil {
        fmt.Fprintf(os.Stderr, "unexpected error: %s\n", err)
        return
    }
    // 人為制造 *os.PathError 類型的錯(cuò)誤。
    r.Close()
    _, err = w.Write([]byte("hi"))
    if err != nil {
        uError := underlyingError(err)
        fmt.Fprintf(os.Stderr, "underlying error: %s (type: %T)\n", uError, uError)
    }
}

函數(shù)underlyingError的作用是,獲取和返回已知的操作系統(tǒng)相關(guān)錯(cuò)誤的潛在錯(cuò)誤值。里面用switch做類型判斷,如果是已知的那些類型,這些類型都會(huì)有Err字段,直接返回Err字段的值。如果case子句都沒有被選中,那么就是一個(gè)其他的類型,直接返回傳入的參數(shù)err,即放棄獲取潛在錯(cuò)誤值。

第二種情況
在Go語(yǔ)言的標(biāo)準(zhǔn)庫(kù)中也有不少以相同方式創(chuàng)建的同類型的錯(cuò)誤值。還拿os包來(lái)說(shuō),其中不少的錯(cuò)誤值都是通過(guò)調(diào)用errors.New函數(shù)來(lái)初始化的,比如:os.ErrClosed、os.ErrInvalid以及os.ErrPermission。與之前的那些錯(cuò)誤類型不同,這幾個(gè)都是已經(jīng)定義好的、確切的錯(cuò)誤值。os包中的代碼有時(shí)候會(huì)把它們當(dāng)做潛在錯(cuò)誤值,封裝進(jìn)前面那些錯(cuò)誤類型的值中。
如果我們?cè)诓僮魑募到y(tǒng)的時(shí)候得到了一個(gè)錯(cuò)誤值,并且知道該值的潛在錯(cuò)誤值肯定是上述值中的某一個(gè),那么就可以用普通的switch語(yǔ)句去做判斷。這里比較難理解,示例如下:

package main

import (
    "fmt"
    "os"
    "os/exec"
)

// underlyingError 會(huì)返回已知的操作系統(tǒng)相關(guān)錯(cuò)誤的潛在錯(cuò)誤值。
func underlyingError(err error) error {
    switch err := err.(type) {
    case *os.PathError:
        return err.Err
    case *os.LinkError:
        return err.Err
    case *os.SyscallError:
        return err.Err
    case *exec.Error:
        return err.Err
    }
    return err
}

func main() {
    paths := []string{
        os.Args[0],           // 當(dāng)前的源碼文件或可執(zhí)行文件。
        "/it/must/not/exist", // 肯定不存在的目錄。
        os.DevNull,           // 肯定存在的目錄。
    }
    printError := func(i int, err error) {
        if err == nil {
            fmt.Println("nil error")
            return
        }
        err = underlyingError(err)  // 先去獲取潛在錯(cuò)誤值
        // 然后對(duì)錯(cuò)誤值進(jìn)行判等來(lái)分辨
        switch err {
        case os.ErrClosed:
            fmt.Printf("case: %s\n", os.ErrClosed)
            fmt.Printf("error(closed)[%d]: %s\n", i, err)
        case os.ErrInvalid:
            fmt.Printf("case: %s\n", os.ErrInvalid)
            fmt.Printf("error(invalid)[%d]: %s\n", i, err)
        case os.ErrPermission:
            fmt.Printf("case: %s\n", os.ErrPermission)
            fmt.Printf("error(permission)[%d]: %s\n", i, err)
        default:
            fmt.Println("case not fount")
            fmt.Printf("error(unknow)[%d]: %s\n", i, err)
        }
    }
    var f *os.File
    var index int
    var err error
    {
        index = 0
        f, err = os.Open(paths[index])
        if err != nil {
            fmt.Printf("unexpected error: %s\n", err)
            return
        }
        // 人為制造潛在錯(cuò)誤為 os.ErrClosed 的錯(cuò)誤。
        f.Close()
        _, err = f.Read([]byte{})
        printError(index, err)
    }
    {
        index = 1
        // 人為制造 os.ErrInvalid 錯(cuò)誤。
        f, _ = os.Open(paths[index])
        _, err = f.Stat()
        printError(index, err)
    }
    {
        index = 2
        // 人為制造潛在錯(cuò)誤為 os.ErrPermission 的錯(cuò)誤。
        _, err = exec.LookPath(paths[index])
        printError(index, err)
    }
    if f != nil {
        f.Close()
    }
}

這里會(huì)用到上一個(gè)例子里的underlyingError函數(shù)。printError變量代表的函數(shù)會(huì)接受一個(gè)error類型的參數(shù)值,該值代表某個(gè)文件操作的相關(guān)錯(cuò)誤。先用underlyingError函數(shù)得到它的潛在錯(cuò)誤值(也可能類型都不符合得到的是原來(lái)的錯(cuò)誤值),然后用switch語(yǔ)句對(duì)錯(cuò)誤值進(jìn)行判等操作。如此來(lái)分辨出具體的錯(cuò)誤。

第三種情況
對(duì)于上面的兩種情況,都有明確的方式來(lái)解決。但是,如果對(duì)一個(gè)錯(cuò)誤的函數(shù)并不清楚,那只能通過(guò)它擁有的錯(cuò)誤信息去判斷了??偸悄軌蛲ㄟ^(guò)錯(cuò)誤值的Error方法拿到它的錯(cuò)誤信息,就是錯(cuò)誤信息的字符串表示形式。還是os包,里面就有做這種判斷的函數(shù),比如:os.IsExist、os.IsNotExist和os.IsPermission。
這里的例子和上面那個(gè)差不多,這次用了if來(lái)做判斷(case和if都可以用),示例如下:

package main

import (
    "fmt"
    "os"
    "os/exec"
    "runtime"
)

func main() {
    paths := []string{
        runtime.GOROOT(),     // 當(dāng)前環(huán)境下的Go語(yǔ)言根目錄。
        "/it/must/not/exist", // 肯定不存在的目錄。
        os.DevNull,           // 肯定存在的目錄。
    }
    printError2 := func(i int, err error) {
        if err == nil {
            fmt.Println("nil error")
            return
        }
        if os.IsExist(err) {
            fmt.Printf("error(exist)[%d]: %s\n", i, err)
        } else if os.IsNotExist(err) {
            fmt.Printf("error(not exist)[%d]: %s\n", i, err)
        } else if os.IsPermission(err) {
            fmt.Printf("error(permission)[%d]: %s\n", i, err)
        } else {
            fmt.Printf("error(other)[%d]: %s\n", i, err)
        }
    }
    var f *os.File
    var index int
    var err error
    {
        index = 0
        err = os.Mkdir(paths[index], 0700)
        printError2(index, err)
    }
    {
        index = 1
        f, err = os.Open(paths[index])
        printError2(index, err)
    }
    {
        index = 2
        _, err = exec.LookPath(paths[index])
        printError2(index, err)
    }
    if f != nil {
        f.Close()
    }
}

這里的代碼里看不出什么,這種情況是獲取錯(cuò)誤的字符串表示形式然后做判斷。這里做判斷的就是os.IsExist、os.IsNotExist和os.IsPermission這3個(gè)函數(shù)。具體看os.IsNotExist做了什么,這個(gè)去源碼里看一下:

// 轉(zhuǎn)去調(diào)用一個(gè)內(nèi)部的方法
func IsNotExist(err error) bool {
    return isNotExist(err)
}

// 再轉(zhuǎn)去調(diào)用字符串分析的方法
func isNotExist(err error) bool {
    return checkErrMessageContent(err, "does not exist", "not found",
        "has been removed", "no parent")
}

// 這個(gè)函數(shù)就是看看錯(cuò)誤信息里是否有特定的字符串
func checkErrMessageContent(err error, msgs ...string) bool {
    if err == nil {
        return false
    }
    // 第一個(gè)例子就開始用的這個(gè)函數(shù),就是從源碼里超的
    err = underlyingError(err)  
    for _, msg := range msgs {
        if contains(err.Error(), msg) {
            return true
        }
    }
    return false
}

這里看到了,我們的代碼里用用做判斷的函數(shù),在源碼里具體做的事情就是獲取錯(cuò)誤信息的字符串表示信息,然后去判斷是否包含了特定的字符串。

總結(jié)

這篇主要就是講錯(cuò)誤類型的判斷,并且用os包舉例了3種判斷錯(cuò)誤類型的方法。
第一種類型斷言,就是直接用類型斷言判斷錯(cuò)誤的類型。error類型是一個(gè)接口類型,這里要用類型斷言判斷出該類型的動(dòng)態(tài)類型,通過(guò)這個(gè)動(dòng)態(tài)類型來(lái)分辨。
第二種錯(cuò)誤值判等,通過(guò)錯(cuò)誤值來(lái)判斷,這里的錯(cuò)誤值是已知的,所以使用判等來(lái)進(jìn)行判斷。
第三種分析錯(cuò)誤值,其實(shí)還是通過(guò)錯(cuò)誤值來(lái)判斷,但是這里的錯(cuò)誤值不確定。例子里用了os包中提供的方法來(lái)進(jìn)行判斷,其底層就是檢查字符串是否包含特定的字符。
另外,用于判斷的語(yǔ)句,類型斷言應(yīng)該還是用case比較合適。其他情況case和if都可以用來(lái)做判斷。

錯(cuò)誤處理(下)

在上篇中,主要是從使用者的角度看“怎樣處理錯(cuò)誤值”。這篇,要從建造者的角度關(guān)心“怎么才能給予使用者恰當(dāng)?shù)腻e(cuò)誤值”。

構(gòu)建錯(cuò)誤值體系的基本方式有兩種:

  • 創(chuàng)建立體的錯(cuò)誤類型體系
  • 創(chuàng)建扁平的錯(cuò)誤值列表

錯(cuò)誤類型體系

由于在Go語(yǔ)言中實(shí)現(xiàn)接口是非侵入式的,所以可以做的很靈活。比如,在標(biāo)準(zhǔn)庫(kù)的net代碼包中,有一個(gè)名為Error的接口類型。它算是內(nèi)建接口類型error的一個(gè)擴(kuò)展接口,因?yàn)閑rror是net.Error的嵌入接口。net.Error接口除了擁有error接口的Error方法外,還有兩者自己什么的方法:Timeout和Temporary。net包中有很多錯(cuò)誤類型都實(shí)現(xiàn)了net.Error接口,比如下面這些:

  • *net.OpError
  • *net.AddrError
  • net.UnknownNetworkError

這些錯(cuò)誤類型就是一個(gè)樹形結(jié)構(gòu),內(nèi)建接口error就是根節(jié)點(diǎn),而net.Error接口就是就是第一級(jí)子節(jié)點(diǎn)。
當(dāng)我們細(xì)看net包中的這些具體錯(cuò)誤類型的實(shí)現(xiàn)時(shí),還會(huì)發(fā)現(xiàn),與os包中的一些錯(cuò)誤類型類似,它們也都有一個(gè)名為Err、類型為error接口類型的字段,代表的也是當(dāng)前錯(cuò)誤的潛在錯(cuò)誤。
所以,這些錯(cuò)誤類型的值纏綿還有另外一種關(guān)系,即:鏈?zhǔn)疥P(guān)系。比如,使用者調(diào)用net.DialTCP之類的函數(shù)是,net包的代碼可能會(huì)返回給他一個(gè) *net.OpError 類型的錯(cuò)誤值,這個(gè)表示用于操作不當(dāng)造成了一個(gè)錯(cuò)誤。同時(shí),這些代碼還會(huì)把一個(gè) *net.AddrError 或 net.UnknownNetworkError 類型的值賦值該錯(cuò)誤值的Err字段,以表示導(dǎo)致這個(gè)錯(cuò)誤的潛在原因。所以,如果此處的潛在錯(cuò)誤值的Err字段也有非nil值,那么就指明了更深層次的錯(cuò)誤原因。如此一級(jí)有一級(jí)就像鏈條指向了問(wèn)題的根源。
以上這些內(nèi)容總結(jié)成一句話就是,用類型建立起樹形結(jié)構(gòu)的錯(cuò)誤體系,用統(tǒng)一字段建立起可追根溯源的鏈?zhǔn)藉e(cuò)誤關(guān)聯(lián)。這是Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)給予我們的優(yōu)秀范本,非常有借鑒意義。
不過(guò)要注意,如果不想讓包外代碼改動(dòng)你返回的錯(cuò)誤值的話,字段名稱一定要小寫??梢酝ㄟ^(guò)暴露某些方法讓包外代碼可以進(jìn)一步獲取錯(cuò)誤信息,比如寫一個(gè)Ere方法返回私有的err字段的值。下面的扁平化方式就不得不暴露字段給包外代碼,這會(huì)帶來(lái)一些問(wèn)題。
小結(jié)
錯(cuò)誤類型體系是立體的,從整體上看它往往呈現(xiàn)出樹形的結(jié)構(gòu)。通過(guò)接口間的嵌套以及接口的實(shí)現(xiàn),就可以構(gòu)建出一棵錯(cuò)誤類型樹。通過(guò)這棵樹,使用者就可以一步步地確定錯(cuò)誤值的種類。
另外,為了追根溯源,還可以在錯(cuò)誤類型中,統(tǒng)一安放一個(gè)可以代表潛在錯(cuò)誤的字段。這叫做鏈?zhǔn)降腻e(cuò)誤關(guān)聯(lián),可以幫助使用者找到錯(cuò)誤的根源。

扁平的錯(cuò)誤值列表

這個(gè)就簡(jiǎn)單得多了。當(dāng)我們只是想預(yù)先創(chuàng)建一些代表已知錯(cuò)誤的錯(cuò)誤值的時(shí)候,用扁平化的方法就是可以了。
由于error是接口類型,所以通過(guò)error.New函數(shù)生成的錯(cuò)誤值只能被賦值給變量,不能給常量。又由于這些變量需要給包外的代碼使用,所以訪問(wèn)權(quán)限只能公開(首字母大寫)。
這就帶來(lái)了一個(gè)問(wèn)題,如果有惡意代碼改變了這些公開變量的值,那么程序的功能就會(huì)受到影響。因?yàn)樵谶@種情況下,我們一般就是通過(guò)判等操作來(lái)判斷拿到的湊之具體是哪一個(gè)錯(cuò)誤,如果值被改變了,就會(huì)影響到判等操作的結(jié)果。這里光看文字沒啥感覺,下面有兩個(gè)示例。
示例1:

package main

import (
    "fmt"
    "os"
    "os/exec"
)

func main() {
    _, err := exec.LookPath(os.DevNull)
    fmt.Printf("error: %s\n", err)
    if execErr, ok := err.(*exec.Error); ok {
        // 這里修改了err里的值,因?yàn)樽侄蚊鸑ame和Err是大寫的
        execErr.Name = os.TempDir()
        execErr.Err = os.ErrNotExist
    }
    fmt.Printf("error: %s\n", err)  // err還是開頭的err,但是值被修改了
}

示例2:

package main

import (
    "fmt"
    "os"
    "errors"
)

func main() {
    err := os.ErrPermission
    // 現(xiàn)在的判斷是正確的
    if os.IsPermission(err) {
        fmt.Printf("error(permission): %s\n", err)
    } else {
        fmt.Printf("error(other): %s\n", err)
    }
    // 由于字段名是大寫的,就可以修改了。
    // os.ErrPermission = os.ErrExist  // 這句怕看不懂,其實(shí)就是改掉原本的值
    os.ErrPermission = errors.New("可以是任意內(nèi)容啊")  // 把原值改掉,改成什么不重要
    // 這次再判斷err類型就不一樣了。err還是開頭的err,但是判斷結(jié)果不一樣了
    if os.IsPermission(err) {
        fmt.Printf("error(permission): %s\n", err)
    } else {
        fmt.Printf("error(other): %s\n", err)
    }
}

這兩個(gè)示例其實(shí)就是一個(gè)情況,字段名大寫了,于是就暴露出來(lái),可以修改了。示例1中if語(yǔ)句內(nèi)是這里所說(shuō)的惡意代碼,示例2中 os.ErrPermission = os.ErrExist 是這里所說(shuō)的惡意代碼。原本以為不改不就OK了?但是在這里的問(wèn)題是err的值被改了,但是沒有看到顯示的修改err的代碼。這個(gè)問(wèn)題就很嚴(yán)重了,問(wèn)題難以被發(fā)現(xiàn)。
解決方案有兩個(gè):
方案一,先私有化變量,然后編寫公開的用于獲取錯(cuò)誤值以及用于判等的錯(cuò)誤值的函數(shù)。就是像上節(jié)錯(cuò)誤類型體系的最后說(shuō)的那么做。
方案二,此方案存在于syscall包中。該包中有一個(gè)類型叫Errno,該類型代表了系統(tǒng)調(diào)用是可能發(fā)生的底層錯(cuò)誤。這個(gè)錯(cuò)誤類型是error接口的實(shí)現(xiàn)類型,同時(shí)也是對(duì)內(nèi)建類型uintptr的再定義類型。由于uintptr可以常量的類型,所以syscall.Error就可以是常量。syscall包中聲明有大量的Errno類型的常量,包外的代碼可以獲取到這些大寫的常量的值,但是無(wú)法改標(biāo)這些常量。
下面是方案二所說(shuō)的,定義了int類型Errno,并且實(shí)現(xiàn)了error接口。自定義這類錯(cuò)誤的示例:

package main

import (
    "fmt"
    "strconv"
)

// Errno 代表某種錯(cuò)誤的類型。
type Errno int

// error接口類型,需要實(shí)現(xiàn)一個(gè)Error方法,這個(gè)方法不接受任何參數(shù),但是會(huì)返回一個(gè)string類型的結(jié)果
func (e Errno) Error() string {
    return "errno " + strconv.Itoa(int(e))
}

func main() {
    const (
        ERR0 = Errno(0)
        ERR1 = Errno(1)
        ERR2 = Errno(2)
    )
    var myErr error = Errno(0)
    switch myErr {
    case ERR0:
        fmt.Println("ERR0")
    case ERR1:
        fmt.Println("ERR1")
    case ERR2:
        fmt.Println("ERR2")
    }
}

小結(jié)
方案一:使用私有變量,使錯(cuò)誤值不可見也不可改,然后編寫公開的函數(shù)返回私有變量的值。
方案二:使用常量,這樣可見但是不可改,需要像syscall那樣聲明新的類型來(lái)實(shí)現(xiàn)error接口。
總之,扁平的錯(cuò)誤值列表雖然相對(duì)簡(jiǎn)單,但是你需要知道其中的隱患以及解決方案。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI