錯(cuò)誤處理到現(xiàn)在為止應(yīng)該已經(jīng)接觸過幾次了。比如,聲明error類型的變量err,或是調(diào)用errors包中的New函數(shù)。
成都創(chuàng)新互聯(lián)從2013年創(chuàng)立,先為張家川回族自治等服務(wù)建站,張家川回族自治等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為張家川回族自治企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。
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ǔ)句,就是被用來檢查后續(xù)操作的前置條件并進(jìn)行相應(yīng)處理的語(yǔ)句。在進(jìn)行錯(cuò)誤處理的時(shí)候經(jīng)常會(huì)用到衛(wèi)述語(yǔ)句,以至于“我的程序滿屏都是衛(wèi)述語(yǔ)句,簡(jiǎn)直是太難看了!”(這里我有同感)。
由于error是一個(gè)接口類型,所以即使同為error類型的錯(cuò)誤值,它們的實(shí)際類型也可能不同。錯(cuò)誤判斷的做法一般是如下的3種:
對(duì)于上面的3種情況,接下來分別展開。
第一種情況
類型在已知范圍內(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包來說,其中不少的錯(cuò)誤值都是通過調(diào)用errors.New函數(shù)來初始化的,比如: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)行判等來分辨
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ò)誤值(也可能類型都不符合得到的是原來的錯(cuò)誤值),然后用switch語(yǔ)句對(duì)錯(cuò)誤值進(jìn)行判等操作。如此來分辨出具體的錯(cuò)誤。
第三種情況
對(duì)于上面的兩種情況,都有明確的方式來解決。但是,如果對(duì)一個(gè)錯(cuò)誤的函數(shù)并不清楚,那只能通過它擁有的錯(cuò)誤信息去判斷了??偸悄軌蛲ㄟ^錯(cuò)誤值的Error方法拿到它的錯(cuò)誤信息,就是錯(cuò)誤信息的字符串表示形式。還是os包,里面就有做這種判斷的函數(shù),比如:os.IsExist、os.IsNotExist和os.IsPermission。
這里的例子和上面那個(gè)差不多,這次用了if來做判斷(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ò)誤信息的字符串表示信息,然后去判斷是否包含了特定的字符串。
這篇主要就是講錯(cuò)誤類型的判斷,并且用os包舉例了3種判斷錯(cuò)誤類型的方法。
第一種類型斷言,就是直接用類型斷言判斷錯(cuò)誤的類型。error類型是一個(gè)接口類型,這里要用類型斷言判斷出該類型的動(dòng)態(tài)類型,通過這個(gè)動(dòng)態(tài)類型來分辨。
第二種錯(cuò)誤值判等,通過錯(cuò)誤值來判斷,這里的錯(cuò)誤值是已知的,所以使用判等來進(jìn)行判斷。
第三種分析錯(cuò)誤值,其實(shí)還是通過錯(cuò)誤值來判斷,但是這里的錯(cuò)誤值不確定。例子里用了os包中提供的方法來進(jìn)行判斷,其底層就是檢查字符串是否包含特定的字符。
另外,用于判斷的語(yǔ)句,類型斷言應(yīng)該還是用case比較合適。其他情況case和if都可以用來做判斷。
在上篇中,主要是從使用者的角度看“怎樣處理錯(cuò)誤值”。這篇,要從建造者的角度關(guān)心“怎么才能給予使用者恰當(dāng)?shù)腻e(cuò)誤值”。
構(gòu)建錯(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接口,比如下面這些:
這些錯(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í)就像鏈條指向了問題的根源。
以上這些內(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)秀范本,非常有借鑒意義。
不過要注意,如果不想讓包外代碼改動(dòng)你返回的錯(cuò)誤值的話,字段名稱一定要小寫??梢酝ㄟ^暴露某些方法讓包外代碼可以進(jìn)一步獲取錯(cuò)誤信息,比如寫一個(gè)Ere方法返回私有的err字段的值。下面的扁平化方式就不得不暴露字段給包外代碼,這會(huì)帶來一些問題。
小結(jié)
錯(cuò)誤類型體系是立體的,從整體上看它往往呈現(xiàn)出樹形的結(jié)構(gòu)。通過接口間的嵌套以及接口的實(shí)現(xiàn),就可以構(gòu)建出一棵錯(cuò)誤類型樹。通過這棵樹,使用者就可以一步步地確定錯(cuò)誤值的種類。
另外,為了追根溯源,還可以在錯(cuò)誤類型中,統(tǒng)一安放一個(gè)可以代表潛在錯(cuò)誤的字段。這叫做鏈?zhǔn)降腻e(cuò)誤關(guān)聯(lián),可以幫助使用者找到錯(cuò)誤的根源。
這個(gè)就簡(jiǎn)單得多了。當(dāng)我們只是想預(yù)先創(chuàng)建一些代表已知錯(cuò)誤的錯(cuò)誤值的時(shí)候,用扁平化的方法就是可以了。
由于error是接口類型,所以通過error.New函數(shù)生成的錯(cuò)誤值只能被賦值給變量,不能給常量。又由于這些變量需要給包外的代碼使用,所以訪問權(quán)限只能公開(首字母大寫)。
這就帶來了一個(gè)問題,如果有惡意代碼改變了這些公開變量的值,那么程序的功能就會(huì)受到影響。因?yàn)樵谶@種情況下,我們一般就是通過判等操作來判斷拿到的湊之具體是哪一個(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è)情況,字段名大寫了,于是就暴露出來,可以修改了。示例1中if語(yǔ)句內(nèi)是這里所說的惡意代碼,示例2中 os.ErrPermission = os.ErrExist
是這里所說的惡意代碼。原本以為不改不就OK了?但是在這里的問題是err的值被改了,但是沒有看到顯示的修改err的代碼。這個(gè)問題就很嚴(yán)重了,問題難以被發(fā)現(xiàn)。
解決方案有兩個(gè):
方案一,先私有化變量,然后編寫公開的用于獲取錯(cuò)誤值以及用于判等的錯(cuò)誤值的函數(shù)。就是像上節(jié)錯(cuò)誤類型體系的最后說的那么做。
方案二,此方案存在于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類型的常量,包外的代碼可以獲取到這些大寫的常量的值,但是無法改標(biāo)這些常量。
下面是方案二所說的,定義了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那樣聲明新的類型來實(shí)現(xiàn)error接口。
總之,扁平的錯(cuò)誤值列表雖然相對(duì)簡(jiǎn)單,但是你需要知道其中的隱患以及解決方案。