錯(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ò)誤處理
作者:黎躍春