Go36-19,20-错误处理

栏目: Go · 发布时间: 6年前

内容简介:错误处理到现在为止应该已经接触过几次了。比如,声明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种:

  1. 对于类型在已知范围内的一系列错误值,一般使用类型断言表达式或类型switch语句来判断
  2. 对于已有相应变量且类型相同的一系列错误值,一般直接使用判等操作来判断
  3. 对于没有相应变量且类型未知的一系列错误值,只能使用其错误信息的字符串表示形式来做判断

对于上面的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接口。

总之,扁平的错误值列表虽然相对简单,但是你需要知道其中的隐患以及解决方案。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

轻量级Django

轻量级Django

茱莉亚·埃尔曼 (Julia Elman)、马克·拉温 (Mark Lavin) / 侯荣涛、吴磊 / 中国电力出版社; 第1版 / 2016-11-1 / 35.6

自Django 创建以来,各种各样的开源社区已经构建了很多Web 框架,比如JavaScript 社区创建的Angular.js 、Ember.js 和Backbone.js 之类面向前端的Web 框架,它们是现代Web 开发中的先驱。Django 从哪里入手来适应这些框架呢?我们如何将客户端MVC 框架整合成为当前的Django 基础架构? 本书讲述如何利用Django 强大的“自支持”功......一起来看看 《轻量级Django》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

SHA 加密
SHA 加密

SHA 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具