Golang(十六)錯(cuò)誤處理

錯(cuò)誤處理

1.1 什么是錯(cuò)誤

錯(cuò)誤是什么?

錯(cuò)誤指出程序中的異常情況。假設(shè)我們正在嘗試打開一個(gè)文件,文件系統(tǒng)中不存在這個(gè)文件。這是一個(gè)異常情況,它表示為一個(gè)錯(cuò)誤。

Go中的錯(cuò)誤也是一種類型。錯(cuò)誤用內(nèi)置的error 類型表示。就像其他類型的,如int,浮動(dòng)64,。錯(cuò)誤值可以存儲(chǔ)在變量中,從函數(shù)中返回,等等。

1.2 演示錯(cuò)誤

讓我們從一個(gè)示例程序開始,這個(gè)程序嘗試打開一個(gè)不存在的文件。

示例代碼:

package main

import (
    "fmt"
    "os"
)

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

在os包中有打開文件的功能函數(shù):

? func Open(name string) (file *File, err error)

如果文件已經(jīng)成功打開,那么Open函數(shù)將返回文件處理。如果在打開文件時(shí)出現(xiàn)錯(cuò)誤,將返回一個(gè)非nil錯(cuò)誤。

如果一個(gè)函數(shù)或方法返回一個(gè)錯(cuò)誤,那么按照慣例,它必須是函數(shù)返回的最后一個(gè)值。因此,Open 函數(shù)返回的值是最后一個(gè)值。

處理錯(cuò)誤的慣用方法是將返回的錯(cuò)誤與nil進(jìn)行比較。nil值表示沒有發(fā)生錯(cuò)誤,而非nil值表示出現(xiàn)錯(cuò)誤。在我們的例子中,我們檢查錯(cuò)誤是否為nil。如果它不是nil,我們只需打印錯(cuò)誤并從主函數(shù)返回。

運(yùn)行結(jié)果:

open /test.txt: No such file or directory

我們得到一個(gè)錯(cuò)誤,說明該文件不存在。

1.3 錯(cuò)誤類型表示

Go 語言通過內(nèi)置的錯(cuò)誤接口提供了非常簡單的錯(cuò)誤處理機(jī)制。

讓我們?cè)偕钊胍稽c(diǎn),看看如何定義錯(cuò)誤類型的構(gòu)建。錯(cuò)誤是一個(gè)帶有以下定義的接口類型

type error interface {
    Error() string
}

它包含一個(gè)帶有Error()字符串的方法。任何實(shí)現(xiàn)這個(gè)接口的類型都可以作為一個(gè)錯(cuò)誤使用。這個(gè)方法提供了對(duì)錯(cuò)誤的描述。

當(dāng)打印錯(cuò)誤時(shí),fmt.Println函數(shù)在內(nèi)部調(diào)用Error() 方法來獲取錯(cuò)誤的描述。這就是錯(cuò)誤描述是如何在一行中打印出來的。

從錯(cuò)誤中提取更多信息的不同方法

既然我們知道錯(cuò)誤是一種接口類型,那么讓我們看看如何提取更多關(guān)于錯(cuò)誤的信息。

在上面的例子中,我們僅僅是打印了錯(cuò)誤的描述。如果我們想要的是導(dǎo)致錯(cuò)誤的文件的實(shí)際路徑。一種可能的方法是解析錯(cuò)誤字符串。這是我們程序的輸出

open /test.txt: No such file or directory

我們可以解析這個(gè)錯(cuò)誤消息并從中獲取文件路徑”/test.txt”。但這是一個(gè)糟糕的方法。在新版本的語言中,錯(cuò)誤描述可以隨時(shí)更改,我們的代碼將會(huì)中斷。

是否有辦法可靠地獲取文件名?答案是肯定的,它可以做到,標(biāo)準(zhǔn)Go庫使用不同的方式提供更多關(guān)于錯(cuò)誤的信息。讓我們一看一看。

1.斷言底層結(jié)構(gòu)類型并從結(jié)構(gòu)字段獲取更多信息

如果仔細(xì)閱讀打開函數(shù)的文檔,可以看到它返回的是PathError類型的錯(cuò)誤。PathError是一個(gè)struct類型,它在標(biāo)準(zhǔn)庫中的實(shí)現(xiàn)如下

type PathError struct {
    Op   string
    Path string
    Err  error
}

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

從上面的代碼中,您可以理解PathError通過聲明錯(cuò)誤()string方法實(shí)現(xiàn)了錯(cuò)誤接口。該方法連接操作、路徑和實(shí)際錯(cuò)誤并返回它。這樣我們就得到了錯(cuò)誤信息

open /test.txt: No such file or directory

PathError結(jié)構(gòu)的路徑字段包含導(dǎo)致錯(cuò)誤的文件的路徑。讓我們修改上面寫的程序,并打印出路徑。

修改代碼:

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")
}

在上面的程序中,我們使用類型斷言獲得錯(cuò)誤接口的基本值。然后我們用錯(cuò)誤來打印路徑.這個(gè)程序輸出

File at path /test.txt failed to open

斷言底層結(jié)構(gòu)類型,并使用方法獲取更多信息
獲得更多信息的第二種方法是斷言底層類型,并通過調(diào)用struct類型的方法獲取更多信息。

示例代碼:

type DNSError struct {
    ...
}

func (e *DNSError) Error() string {
    ...
}
func (e *DNSError) Timeout() bool {
    ...
}
func (e *DNSError) Temporary() bool {
    ...
}

從上面的代碼中可以看到,DNSError struct有兩個(gè)方法Timeout() bool和Temporary() bool,它們返回一個(gè)布爾值,表示錯(cuò)誤是由于超時(shí)還是臨時(shí)的。

讓我們編寫一個(gè)斷言*DNSError類型的程序,并調(diào)用這些方法來確定錯(cuò)誤是臨時(shí)的還是超時(shí)的。

package main

import (
    "fmt"
    "net"
)

func main() {
    addr, err := net.LookupHost("golangbot123.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)
}

在上面的程序中,我們正在嘗試獲取一個(gè)無效域名的ip地址,這是一個(gè)無效的域名。golangbot123.com。我們通過聲明它來輸入*net.DNSError來獲得錯(cuò)誤的潛在價(jià)值。

在我們的例子中,錯(cuò)誤既不是暫時(shí)的,也不是由于超時(shí),因此程序會(huì)打印出來

generic error:  lookup golangbot123.com: no such host

如果錯(cuò)誤是臨時(shí)的或超時(shí)的,那么相應(yīng)的If語句就會(huì)執(zhí)行,我們可以適當(dāng)?shù)靥幚硭?/p>

直接比較

獲得更多關(guān)于錯(cuò)誤的詳細(xì)信息的第三種方法是直接與類型錯(cuò)誤的變量進(jìn)行比較。讓我們通過一個(gè)例子來理解這個(gè)問題。

filepath包的Glob函數(shù)用于返回與模式匹配的所有文件的名稱。當(dāng)模式出現(xiàn)錯(cuò)誤時(shí),該函數(shù)將返回一個(gè)錯(cuò)誤ErrBadPattern。

在filepath包中定義了ErrBadPattern,如下所述:

var ErrBadPattern = errors.New("syntax error in pattern")

errors.New()用于創(chuàng)建新的錯(cuò)誤。

當(dāng)模式出現(xiàn)錯(cuò)誤時(shí),由Glob函數(shù)返回ErrBadPattern。

讓我們寫一個(gè)小程序來檢查這個(gè)錯(cuò)誤:

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    files, error := filepath.Glob("[")
    if error != nil && error == filepath.ErrBadPattern {
        fmt.Println(error)
        return
    }
    fmt.Println("matched files", files)
}

運(yùn)行結(jié)果:

syntax error in pattern

不要忽略錯(cuò)誤

永遠(yuǎn)不要忽略一個(gè)錯(cuò)誤。忽視錯(cuò)誤會(huì)招致麻煩。讓我重新編寫一個(gè)示例,該示例列出了與模式匹配的所有文件的名稱,而忽略了錯(cuò)誤處理代碼。

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    files, _ := filepath.Glob("[")
    fmt.Println("matched files", files)
}

我們從前面的例子中已經(jīng)知道模式是無效的。我忽略了Glob函數(shù)返回的錯(cuò)誤,方法是使用行號(hào)中的空白標(biāo)識(shí)符。

matched files []

由于我們忽略了這個(gè)錯(cuò)誤,輸出看起來好像沒有文件匹配這個(gè)模式,但是實(shí)際上這個(gè)模式本身是畸形的。所以不要忽略錯(cuò)誤。

1.4 自定義錯(cuò)誤

創(chuàng)建自定義錯(cuò)誤的最簡單方法是使用錯(cuò)誤包的新功能。

在使用新函數(shù)創(chuàng)建自定義錯(cuò)誤之前,讓我們了解它是如何實(shí)現(xiàn)的。下面提供了錯(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
  }

既然我們知道了新函數(shù)是如何工作的,那么就讓我們?cè)谧约旱某绦蛑惺褂盟鼇韯?chuàng)建一個(gè)自定義錯(cuò)誤。

我們將創(chuàng)建一個(gè)簡單的程序,計(jì)算一個(gè)圓的面積,如果半徑為負(fù),將返回一個(gè)錯(cuò)誤。

package main

import (
    "errors"
    "fmt"
    "math"
)

func circleArea(radius float64) (float64, error) {
    if radius < 0 {
        return 0, errors.New("Area calculation failed, radius is less than zero")
    }
    return math.Pi * radius * radius, nil
}

func main() {
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}

運(yùn)行結(jié)果:

Area calculation failed, radius is less than zero

使用Errorf向錯(cuò)誤添加更多信息

上面的程序運(yùn)行得很好,但是如果我們打印出導(dǎo)致錯(cuò)誤的實(shí)際半徑,那就更好了。這就是fmt包的Errorf函數(shù)的用武之地。這個(gè)函數(shù)根據(jù)一個(gè)格式說明器格式化錯(cuò)誤,并返回一個(gè)字符串作為值來滿足錯(cuò)誤。

使用Errorf函數(shù),修改程序。

package main

import (
    "fmt"
    "math"
)

func circleArea(radius float64) (float64, error) {
    if radius < 0 {
        return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
    }
    return math.Pi * radius * radius, nil
}

func main() {
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}

運(yùn)行結(jié)果:

Area calculation failed, radius -20.00 is less than zero

使用struct類型和字段提供關(guān)于錯(cuò)誤的更多信息

還可以使用將錯(cuò)誤接口實(shí)現(xiàn)為錯(cuò)誤的struct類型。這給我們提供了更多的錯(cuò)誤處理的靈活性。在我們的示例中,如果我們想要訪問導(dǎo)致錯(cuò)誤的半徑,那么現(xiàn)在唯一的方法是解析錯(cuò)誤描述區(qū)域計(jì)算失敗,半徑-20.00小于零。這不是一種正確的方法,因?yàn)槿绻枋霭l(fā)生了變化,我們的代碼就會(huì)中斷。

我們將使用在前面的教程中解釋的標(biāo)準(zhǔn)庫的策略,在“斷言底層結(jié)構(gòu)類型并從struct字段獲取更多信息”,并使用struct字段來提供對(duì)導(dǎo)致錯(cuò)誤的半徑的訪問。我們將創(chuàng)建一個(gè)實(shí)現(xiàn)錯(cuò)誤接口的struct類型,并使用它的字段來提供關(guān)于錯(cuò)誤的更多信息。

第一步是創(chuàng)建一個(gè)struct類型來表示錯(cuò)誤。錯(cuò)誤類型的命名約定是,名稱應(yīng)該以文本Error結(jié)束。讓我們把struct類型命名為areaError

type areaError struct {
    err    string
    radius float64
}

上面的struct類型有一個(gè)字段半徑,它存儲(chǔ)了為錯(cuò)誤負(fù)責(zé)的半徑的值,并且錯(cuò)誤字段存儲(chǔ)了實(shí)際的錯(cuò)誤消息。

下一步,是實(shí)現(xiàn)error 接口

func (e *areaError) Error() string {
    return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}

在上面的代碼片段中,我們使用一個(gè)指針接收器區(qū)域錯(cuò)誤來實(shí)現(xiàn)錯(cuò)誤接口的Error() string方法。這個(gè)方法打印出半徑和錯(cuò)誤描述。

package main

import (
    "fmt"
    "math"
)

type areaError struct {
    err    string
    radius float64
}

func (e *areaError) Error() string {
    return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}

func circleArea(radius float64) (float64, error) {
    if radius < 0 {
        return 0, &areaError{"radius is negative", radius}
    }
    return math.Pi * radius * radius, nil
}

func main() {
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        if err, ok := err.(*areaError); ok {
            fmt.Printf("Radius %0.2f is less than zero", err.radius)
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of rectangle1 %0.2f", area)
}

程序輸出:

Radius -20.00 is less than zero

使用結(jié)構(gòu)類型的方法提供關(guān)于錯(cuò)誤的更多信息

在本節(jié)中,我們將編寫一個(gè)程序來計(jì)算矩形的面積。如果長度或?qū)挾刃∮?,這個(gè)程序?qū)⑤敵鲆粋€(gè)錯(cuò)誤。

第一步是創(chuàng)建一個(gè)結(jié)構(gòu)來表示錯(cuò)誤。

type areaError struct {
    err    string //error description
    length float64 //length which caused the error
    width  float64 //width which caused the error
}

上面的錯(cuò)誤結(jié)構(gòu)類型包含一個(gè)錯(cuò)誤描述字段,以及導(dǎo)致錯(cuò)誤的長度和寬度。

現(xiàn)在我們有了錯(cuò)誤類型,讓我們實(shí)現(xiàn)錯(cuò)誤接口,并在錯(cuò)誤類型上添加一些方法來提供關(guān)于錯(cuò)誤的更多信息。

func (e *areaError) Error() string {
    return e.err
}

func (e *areaError) lengthNegative() bool {
    return e.length < 0
}

func (e *areaError) widthNegative() bool {
    return e.width < 0
}

在上面的代碼片段中,我們返回Error() string 方法的錯(cuò)誤描述。當(dāng)長度小于0時(shí),lengthNegative() bool方法返回true;當(dāng)寬度小于0時(shí),widthNegative() bool方法返回true。這兩種方法提供了更多關(guān)于誤差的信息,在這種情況下,他們說面積計(jì)算是否失敗,因?yàn)殚L度是負(fù)的,還是寬度為負(fù)的。因此,我們使用了struct錯(cuò)誤類型的方法來提供更多關(guān)于錯(cuò)誤的信息。

下一步是寫出面積計(jì)算函數(shù)。

func rectArea(length, width float64) (float64, error) {
    err := ""
    if length < 0 {
        err += "length is less than zero"
    }
    if width < 0 {
        if err == "" {
            err = "width is less than zero"
        } else {
            err += ", width is less than zero"
        }
    }
    if err != "" {
        return 0, &areaError{err, length, width}
    }
    return length * width, nil
}

上面的rectArea函數(shù)檢查長度或?qū)挾仁欠裥∮?,如果它返回一個(gè)錯(cuò)誤消息,則返回矩形的面積為nil。

主函數(shù):

func main() {
    length, width := -5.0, -9.0
    area, err := rectArea(length, width)
    if err != nil {
        if err, ok := err.(*areaError); ok {
            if err.lengthNegative() {
                fmt.Printf("error: length %0.2f is less than zero\n", err.length)

            }
            if err.widthNegative() {
                fmt.Printf("error: width %0.2f is less than zero\n", err.width)

            }
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Println("area of rect", area)
}

運(yùn)行結(jié)果:

error: length -5.00 is less than zero
error: width -9.00 is less than zero

其他案例:

函數(shù)通常在最后的返回值中返回錯(cuò)誤信息。使用errors.New 可返回一個(gè)錯(cuò)誤信息

package main

import (
    "fmt"
)

// 定義一個(gè) DivideError 結(jié)構(gòu)
type DivideError struct {
    dividee int
    divider int
}

// 實(shí)現(xiàn)   `error` 接口
func (de *DivideError) Error() string {
    strFormat := `
    Cannot proceed, the divider is zero.
    dividee: %d
    divider: 0
`
    return fmt.Sprintf(strFormat, de.dividee)
}

// 定義 `int` 類型除法運(yùn)算的函數(shù)
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
    if varDivider == 0 {
        dData := DivideError{
            dividee: varDividee,
            divider: varDivider,
        }
        errorMsg = dData.Error()
        return
    } else {
        return varDividee / varDivider, ""
    }

}

func main() {

    // 正常情況
    if result, errorMsg := Divide(100, 10); errorMsg == "" {
        fmt.Println("100/10 = ", result)
    }
    // 當(dāng)被除數(shù)為零的時(shí)候會(huì)返回錯(cuò)誤信息
    if _, errorMsg := Divide(100, 0); errorMsg != "" {
        fmt.Println("errorMsg is: ", errorMsg)
    }
}

結(jié)果

100/10 =  10
errorMsg is:
    Cannot proceed, the divider is zero.
    dividee: 100
    divider: 0

原文:第16章-錯(cuò)誤處理
作者:黎躍春

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 錯(cuò)誤處理 1.1 什么是錯(cuò)誤 錯(cuò)誤是什么? 錯(cuò)誤指出程序中的異常情況。假設(shè)我們正在嘗試打開一個(gè)文件,文件系統(tǒng)中不存...
    Venture_Mark閱讀 247評(píng)論 0 0
  • ??由于 JavaScript 本身是動(dòng)態(tài)語言,而且多年來一直沒有固定的開發(fā)工具,因此人們普遍認(rèn)為它是一種最難于調(diào)...
    霜天曉閱讀 823評(píng)論 0 1
  • 30. 錯(cuò)誤處理 什么是錯(cuò)誤? 錯(cuò)誤表示程序中出現(xiàn)了異常情況。比如當(dāng)我們?cè)噲D打開一個(gè)文件時(shí),文件系統(tǒng)里卻并沒有這個(gè)...
    瀧汰泱閱讀 100評(píng)論 0 0
  • 夜鶯2517閱讀 128,103評(píng)論 1 9
  • 版本:ios 1.2.1 亮點(diǎn): 1.app角標(biāo)可以實(shí)時(shí)更新天氣溫度或選擇空氣質(zhì)量,建議處女座就不要選了,不然老想...
    我就是沉沉閱讀 7,380評(píng)論 1 6

友情鏈接更多精彩內(nèi)容