内容简介:错误处理到现在为止应该已经接触过几次了。比如,声明error类型的变量err,或是调用errors包中的New函数。error类型是一个接口类型,是一个Go语言的内建类型。在这个接口类型的声明中只包含了一个方法Error。这个方法不接受任何参数,但是会返回一个string类型的结果。它的作用是返回错误信息的字符串表示形式。使用error类型的方式通常是,在函数声明的结果列表的最后,声明一个该类型的结果,同时在调用这个函数之后,先判断它返回的最后一个结果值是否“不为nil”。如果值“不为nil”,就需要进入错
错误处理(上)
错误处理到现在为止应该已经接触过几次了。比如,声明error类型的变量err,或是调用errors包中的New函数。
error类型
error类型是一个接口类型,是一个 Go 语言的内建类型。在这个接口类型的声明中只包含了一个方法Error。这个方法不接受任何参数,但是会返回一个string类型的结果。它的作用是返回错误信息的字符串表示形式。使用error类型的方式通常是,在函数声明的结果列表的最后,声明一个该类型的结果,同时在调用这个函数之后,先判断它返回的最后一个结果值是否“不为nil”。如果值“不为nil”,就需要进入错误处理。否则就是继续正常的流程。示例如下:
package main import "fmt" func echo(request string) (response string, err error) { if request == "" { err = fmt.Errorf("空字符串") // 这里底层也是调用下面的New,但是支持字符串格式化 // 如果是纯字符串,可以直接调用errors包里的New函数 // 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函数和main函数中,我都使用到了卫述语句。卫述语句,就是被用来检查后续操作的前置条件并进行相应处理的语句。在进行错误处理的时候经常会用到卫述语句,以至于“我的程序满屏都是卫述语句,简直是太难看了!”(这里我有同感)。
错误判断
由于error是一个接口类型,所以即使同为error类型的错误值,它们的实际类型也可能不同。错误判断的做法一般是如下的3种:
- 对于类型在已知范围内的一系列错误值,一般使用类型断言表达式或类型switch语句来判断
- 对于已有相应变量且类型相同的一系列错误值,一般直接使用判等操作来判断
- 对于没有相应变量且类型未知的一系列错误值,只能使用其错误信息的字符串表示形式来做判断
对于上面的3种情况,接下来分别展开。
类型在已知范围内的错误值是最容易分辨的。拿os包中的几个代表错误的类型os.PathError、os.LinkError、os.SyscallError和os/exec.Error举例,它们的指针类型都是error接口的实现类型,同时它们也都包含了一个名叫Err,类型为error接口类型的代表潜在错误的字段。
如果得到一个error类型值,并且知道该值的实际类型肯定是它们中的某一个,那就可以用类型switch语句去做判断。示例如下:
package main import ( "fmt" "os" "os/exec" ) // underlyingError 会返回已知的操作系统相关错误的潜在错误值。 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 类型的错误。 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) } }
函数underlyingError的作用是,获取和返回已知的操作系统相关错误的潜在错误值。里面用switch做类型判断,如果是已知的那些类型,这些类型都会有Err字段,直接返回Err字段的值。如果case子句都没有被选中,那么就是一个其他的类型,直接返回传入的参数err,即放弃获取潜在错误值。
在Go语言的标准库中也有不少以相同方式创建的同类型的错误值。还拿os包来说,其中不少的错误值都是通过调用errors.New函数来初始化的,比如:os.ErrClosed、os.ErrInvalid以及os.ErrPermission。与之前的那些错误类型不同,这几个都是已经定义好的、确切的错误值。os包中的代码有时候会把它们当做潜在错误值,封装进前面那些错误类型的值中。
如果我们在操作文件系统的时候得到了一个错误值,并且知道该值的潜在错误值肯定是上述值中的某一个,那么就可以用普通的switch语句去做判断。这里比较难理解,示例如下:
package main import ( "fmt" "os" "os/exec" ) // underlyingError 会返回已知的操作系统相关错误的潜在错误值。 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], // 当前的源码文件或可执行文件。 "/it/must/not/exist", // 肯定不存在的目录。 os.DevNull, // 肯定存在的目录。 } printError := func(i int, err error) { if err == nil { fmt.Println("nil error") return } err = underlyingError(err) // 先去获取潜在错误值 // 然后对错误值进行判等来分辨 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 } // 人为制造潜在错误为 os.ErrClosed 的错误。 f.Close() _, err = f.Read([]byte{}) printError(index, err) } { index = 1 // 人为制造 os.ErrInvalid 错误。 f, _ = os.Open(paths[index]) _, err = f.Stat() printError(index, err) } { index = 2 // 人为制造潜在错误为 os.ErrPermission 的错误。 _, err = exec.LookPath(paths[index]) printError(index, err) } if f != nil { f.Close() } }
这里会用到上一个例子里的underlyingError函数。printError变量代表的函数会接受一个error类型的参数值,该值代表某个文件操作的相关错误。先用underlyingError函数得到它的潜在错误值(也可能类型都不符合得到的是原来的错误值),然后用switch语句对错误值进行判等操作。如此来分辨出具体的错误。
第三种情况
对于上面的两种情况,都有明确的方式来解决。但是,如果对一个错误的函数并不清楚,那只能通过它拥有的错误信息去判断了。总是能够通过错误值的Error方法拿到它的错误信息,就是错误信息的字符串表示形式。还是os包,里面就有做这种判断的函数,比如:os.IsExist、os.IsNotExist和os.IsPermission。
这里的例子和上面那个差不多,这次用了if来做判断(case和if都可以用),示例如下:
package main import ( "fmt" "os" "os/exec" "runtime" ) func main() { paths := []string{ runtime.GOROOT(), // 当前环境下的Go语言根目录。 "/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() } }
这里的代码里看不出什么,这种情况是获取错误的字符串表示形式然后做判断。这里做判断的就是os.IsExist、os.IsNotExist和os.IsPermission这3个函数。具体看os.IsNotExist做了什么,这个去源码里看一下:
// 转去调用一个内部的方法 func IsNotExist(err error) bool { return isNotExist(err) } // 再转去调用字符串分析的方法 func isNotExist(err error) bool { return checkErrMessageContent(err, "does not exist", "not found", "has been removed", "no parent") } // 这个函数就是看看错误信息里是否有特定的字符串 func checkErrMessageContent(err error, msgs ...string) bool { if err == nil { return false } // 第一个例子就开始用的这个函数,就是从源码里超的 err = underlyingError(err) for _, msg := range msgs { if contains(err.Error(), msg) { return true } } return false }
这里看到了,我们的代码里用用做判断的函数,在源码里具体做的事情就是获取错误信息的字符串表示信息,然后去判断是否包含了特定的字符串。
总结
这篇主要就是讲错误类型的判断,并且用os包举例了3种判断错误类型的方法。
第一种 类型断言 ,就是直接用类型断言判断错误的类型。error类型是一个接口类型,这里要用类型断言判断出该类型的动态类型,通过这个动态类型来分辨。
第二种 错误值判等 ,通过错误值来判断,这里的错误值是已知的,所以使用判等来进行判断。
第三种 分析错误值 ,其实还是通过错误值来判断,但是这里的错误值不确定。例子里用了os包中提供的方法来进行判断,其底层就是检查字符串是否包含特定的字符。
另外,用于判断的语句,类型断言应该还是用case比较合适。其他情况case和if都可以用来做判断。
错误处理(下)
在上篇中,主要是从使用者的角度看“怎样处理错误值”。这篇,要从建造者的角度关心“怎么才能给予使用者恰当的错误值”。
构建错误值体系的基本方式有两种:
- 创建立体的错误类型体系
- 创建扁平的错误值列表
错误类型体系
由于在Go语言中实现接口是非侵入式的,所以可以做的很灵活。比如,在标准库的net代码包中,有一个名为Error的接口类型。它算是内建接口类型error的一个扩展接口,因为error是net.Error的嵌入接口。net.Error接口除了拥有error接口的Error方法外,还有两者自己什么的方法:Timeout和Temporary。net包中有很多错误类型都实现了net.Error接口,比如下面这些:
- *net.OpError
- *net.AddrError
- net.UnknownNetworkError
这些错误类型就是一个树形结构,内建接口error就是根节点,而net.Error接口就是就是第一级子节点。
当我们细看net包中的这些具体错误类型的实现时,还会发现,与os包中的一些错误类型类似,它们也都有一个名为Err、类型为error接口类型的字段,代表的也是当前错误的潜在错误。
所以,这些错误类型的值缠绵还有另外一种关系,即:链式关系。比如,使用者调用net.DialTCP之类的函数是,net包的代码可能会返回给他一个 *net.OpError 类型的错误值,这个表示用于操作不当造成了一个错误。同时,这些代码还会把一个 *net.AddrError 或 net.UnknownNetworkError 类型的值赋值该错误值的Err字段,以表示导致这个错误的潜在原因。所以,如果此处的潜在错误值的Err字段也有非nil值,那么就指明了更深层次的错误原因。如此一级有一级就像链条指向了问题的根源。
以上这些内容总结成一句话就是,用类型建立起树形结构的错误体系,用统一字段建立起可追根溯源的链式错误关联。这是Go语言标准库给予我们的优秀范本,非常有借鉴意义。
不过要注意,如果不想让包外代码改动你返回的错误值的话,字段名称一定要小写。可以通过暴露某些方法让包外代码可以进一步获取错误信息,比如写一个Ere方法返回私有的err字段的值。下面的扁平化方式就不得不暴露字段给包外代码,这会带来一些问题。
小结错误类型体系是立体的,从整体上看它往往呈现出树形的结构。通过接口间的嵌套以及接口的实现,就可以构建出一棵错误类型树。通过这棵树,使用者就可以一步步地确定错误值的种类。
另外,为了追根溯源,还可以在错误类型中,统一安放一个可以代表潜在错误的字段。这叫做链式的错误关联,可以帮助使用者找到错误的根源。
扁平的错误值列表
这个就简单得多了。当我们只是想预先创建一些代表已知错误的错误值的时候,用扁平化的方法就是可以了。
由于error是接口类型,所以通过error.New函数生成的错误值只能被赋值给变量,不能给常量。又由于这些变量需要给包外的代码使用,所以访问权限只能公开(首字母大写)。
这就带来了一个问题,如果有恶意代码改变了这些公开变量的值,那么程序的功能就会受到影响。因为在这种情况下,我们一般就是通过判等操作来判断拿到的凑之具体是哪一个错误,如果值被改变了,就会影响到判等操作的结果。这里光看文字没啥感觉,下面有两个示例。
示例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里的值,因为字段名Name和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 // 现在的判断是正确的 if os.IsPermission(err) { fmt.Printf("error(permission): %s\n", err) } else { fmt.Printf("error(other): %s\n", err) } // 由于字段名是大写的,就可以修改了。 // os.ErrPermission = os.ErrExist // 这句怕看不懂,其实就是改掉原本的值 os.ErrPermission = errors.New("可以是任意内容啊") // 把原值改掉,改成什么不重要 // 这次再判断err类型就不一样了。err还是开头的err,但是判断结果不一样了 if os.IsPermission(err) { fmt.Printf("error(permission): %s\n", err) } else { fmt.Printf("error(other): %s\n", err) } }
这两个示例其实就是一个情况,字段名大写了,于是就暴露出来,可以修改了。示例1中if语句内是这里所说的恶意代码,示例2中 os.ErrPermission = os.ErrExist
是这里所说的恶意代码。原本以为不改不就OK了?但是在这里的问题是err的值被改了,但是没有看到显示的修改err的代码。这个问题就很严重了,问题难以被发现。
解决方案有两个:
方案一,先私有化变量,然后编写公开的用于获取错误值以及用于判等的错误值的函数。就是像上节错误类型体系的最后说的那么做。
方案二,此方案存在于syscall包中。该包中有一个类型叫Errno,该类型代表了系统调用是可能发生的底层错误。这个错误类型是error接口的实现类型,同时也是对内建类型uintptr的再定义类型。由于uintptr可以常量的类型,所以syscall.Error就可以是常量。syscall包中声明有大量的Errno类型的常量,包外的代码可以获取到这些大写的常量的值,但是无法改标这些常量。
下面是方案二所说的,定义了int类型Errno,并且实现了error接口。自定义这类错误的示例:
package main import ( "fmt" "strconv" ) // Errno 代表某种错误的类型。 type Errno int // error接口类型,需要实现一个Error方法,这个方法不接受任何参数,但是会返回一个string类型的结果 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") } }
方案一:使用私有变量,使错误值不可见也不可改,然后编写公开的函数返回私有变量的值。
方案二:使用常量,这样可见但是不可改,需要像syscall那样声明新的类型来实现error接口。
总之,扁平的错误值列表虽然相对简单,但是你需要知道其中的隐患以及解决方案。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Go程序设计语言
艾伦 A. A. 多诺万 / 李道兵、高博、庞向才、金鑫鑫、林齐斌 / 机械工业出版社 / 2017-5 / 79
本书由《C程序设计语言》的作者Kernighan和谷歌公司Go团队主管Alan Donovan联袂撰写,是学习Go语言程序设计的指南。本书共13章,主要内容包括:Go的基础知识、基本结构、基本数据类型、复合数据类型、函数、方法、接口、goroutine、通道、共享变量的并发性、包、go工具、测试、反射等。 本书适合作为计算机相关专业的教材,也可供Go语言爱好者阅读。一起来看看 《Go程序设计语言》 这本书的介绍吧!