标签 Gopher 下的文章

Go语言随机测试工具go-fuzz

Go 1.5发布时,前Intel Black Belt级工程师,现Google工程师Dmitry Vyukov同时发布了Go语言随机测试工具go-fuzz。在 GopherCon2015大会上,Dmitry Vyukov在其名为“[Go Dynamic Tools]”的presentation中着重介绍了go-fuzz

go-fuzz是一款随机测试(Random testing)工具。对于随机测试想必很多人都比较陌生,我也不例外。至少在接触go-fuzz之前,我从未在golang或其他编程语言中使用过类似的测试工具(c/c++开发者可以使用afl-fuzz)。按照维基百科的说法:随机测试就是指半自动或自动地为程序提供非法的、非预期、随机的数据,并监控程序在这些输入数据 下的crash、内置断言、内存泄露等情况。随机测试的研究始于1988年的Barton Miller,到目前为止已经有许多理论支撑,不过这里不会涉及,有兴趣的、想深入的朋友可以跟随维基百科中的链接自行学习。

在开始go-fuzz之前,我们需要认识到随机测试的位置和意义:
* 首先它是软件测试技术的一个重要分支,与单元测试等互为补充;
* 其次随机测试不是什么银弹,它有其适用的范围。随机测试最适合那些处理复杂输入数据的程序,比如文件格式解析、网络协议解析、人机交互界面入口等。
* 最后,并非所有编程语言都有类似的工具支撑,gopher很幸运,Dmitry Vyukov为我们带来了go-fuzz。

接下来就让我们回到go-fuzz这个正题上来。

一、Why go-fuzz

go-fuzz之所以吸引眼球,源于Dmitry Vyukov在使用go-fuzz对go标准库以及其他第三方开源库进行测试后的“惊人的战果”。Dmitry在其slide中展示了这些战果:

60 tests
137 bugs in std lib (70 fixed)
165 elsewhere (47 in gccgo, 30 in golang.org/x, 42 in freetype-go, protobuf, http2, bson)

Dmitry Vyukov的go-fuzz实际上也是基于前面提到的afl-fuzz的逻辑 的基础上设计和实现的。不同的是在使用的时候,afl-fuzz对于每个input case都会fork一个process,而go-fuzz则是通过将input case中的data传给一个Fuzz函数:

func Fuzz(data []byte) int

这样就无需反复重启程序。

go-fuzz进一步完善了go开发测试工具集,很多一线公司(比如cloudflare)已经开始使用go-fuzz来测试自己的产品,提高产品质量了。

二、原理

Dmitry在其slide中将go-fuzz的工作流程归纳如下:

 -> 生成随机数据
 -> 输入给程序
 -> 观察是否有crash
 -> 如果发现crash,则获益
  之后开发者根据crash的结果,尝试fix bug,并
  添加针对这个bug的单元测试case。

go-fuzz一旦运行起来,将会是一个infinite loop(一种遗传算法),该loop的伪代码在slide也有给出:

Instrument program for code coverage
Collect initial corpus of inputs  //收集初始输入数据语料(位于workdir的corpus目录下)
for {
    //从corpus中读取语料并随机变化
    Randomly mutate an input from the corpus

    //执行Fuzz,收集覆盖范围
    Execute and collect coverage

    //如果输入数据提供了新的coverage,则将该数据存入语料库(corpus)
    If the input gives new coverage, add it to corpus
}

go-fuzz内部实现了多种对初始语料库中输入数据的mutation策略:

* Insert/remove/duplicate/copy a random range of random bytes.
* Bit flip.
* Swap 2 bytes.
* Set a byte to a random value.
* Add/subtract from a byte/uint16/uint32/uint64 (le/be).
* Replace a byte/uint16/uint32 with an interesting value (le/be).
* Replace an ascii digit/number with another digit/number.
* Splice another input.
* Insert a part of another input.
* Insert a string/int literal.
* Replace with string/int literal.

三、使用方法

1、安装go-fuzz

使用go-fuzz需要安装两个重要工具:go-fuzz-build和go-fuzz,通过标准go get就可以安装它们:

$ go get github.com/dvyukov/go-fuzz/go-fuzz
$ go get github.com/dvyukov/go-fuzz/go-fuzz-build

对于国内用户而言,由于go-fuzz并未使用go 1.5引入的vendor机制, 而其依赖的一些包却在墙外,因此可能会遇到些麻烦。

go get自动安装两个工具到$GOROOT/bin或$GOPATH/bin,因此你需要确保你的Path环境变量下包含了这两个路径。

2、带有fuzz test的项目组织

假设我们的待测试的go包名为foo,路径为$GOPATH/src/github.com/bigwhite/fuzzexamples/foo。为了应用go- fuzz,我们一般会在foo下创建fuzz.go源文件,其内容模板如下:

// +build gofuzz

package foo

func Fuzz(data []byte) int {
    ... ...
}

go-fuzz在构建用于执行fuzz test的驱动binary文件时,会搜索带有”+build gofuzz” directive的源文件以及其中的Fuzz函数。如果foo包下没有该文件,你在执行go-fuzz-build时,会得到类似如下的错误日志:

$go-fuzz-build github.com/bigwhite/fuzzexamples/foo
failed to execute go build: exit status 2
# go-fuzz-main
/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-fuzz-build641745751/src/go-fuzz-main/main.go:10: undefined: foo.Fuzz

有些时候待测试包内功能很多,一个Fuzz函数不够,我们可以参考go-fuzz中example中的目录组织形式来应对:

github.com/bigwhite/fuzzexamples/foo/fuzztest]$tree
.
├── fuzz1
│   ├── corpus
│   ├── fuzz.go
│   └── gen
│       └── main.go
└── fuzz2
    ├── corpus
    ├── fuzz.go
    └── gen
        └── main.go
 ... ...

这其中的fuzz1、fuzz2…. fuzzN各自为一个go-fuzz单元,如果要应用go-fuzz,则可像下面这样执行:

$ cd fuzz1
$ go-fuzz-build github.com/bigwhite/fuzzexamples/foo/fuzztest/fuzz1
$ go-fuzz -bin=./foo-fuzz.zip -workdir=./

.. ...

$ cd fuzz2
$ go-fuzz-build github.com/bigwhite/fuzzexamples/foo/fuzztest/fuzz2
$ go-fuzz -bin=./foo-fuzz.zip -workdir=./

每个go-fuzz单元下有一套”固定”目录组合:

├── fuzz1
│   ├── corpus
│   ├── fuzz.go
│   └── gen
│       └── main.go

corpus为存放输入数据语料的目录,在go-fuzz执行之前,可放入初始语料;
fuzz.go为包含Fuzz函数的源码文件;
gen目录中包含手工生成初始语料的main.go代码。

在后续的示例中,我们会展示细节。

3、go-fuzz-build

go-fuzz-build会根据Fuzz函数构建一个用于go-fuzz执行的zip包(PACKAGENAME-fuzz.zip),包里包含了用途不同的三 个文件:

-rw-r--r--   1 tony  staff  3902136 12 31  1979 cover.exe
-rw-r--r--   1 tony  staff  3211816 12 31  1979 metadata
-rw-r--r--   1 tony  staff  5031496 12 31  1979 sonar.exe

按照作者slide中的说法,各个二进制程序的功能如下:
cover.exe – coverage instrumented binary
sonar.exe – sonar instrumented binary
metadata – coverage and sonar metadata, int and string literals

不过对于使用者来说,我们不必过于关心它们,点到为止。

4、执行go-fuzz

一旦生成了foo-fuzz.zip,我们就可以执行针对fuzz1的fuzz test。

$ cd fuzz1
$ go-fuzz -bin=./foo-fuzz.zip -workdir=./
2015/12/08 17:51:48 slaves: 4, corpus: 8 (1s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s
2015/12/08 17:51:51 slaves: 4, corpus: 9 (2s ago), crashers: 0, restarts: 1/3851, execs: 11553 (1924/sec), cover: 143, uptime: 6s
2015/12/08 17:51:54 slaves: 4, corpus: 9 (5s ago), crashers: 0, restarts: 1/3979, execs: 47756 (5305/sec), cover: 143, uptime: 9s
... ...

如果corpus中没有初始语料数据,那么go-fuzz也会自行生成相关数据传递给Fuzz函数,并且采用遗传算法,不断基于corpus中的语料生成新的输入语料。go-fuzz作者建议corpus初始时放入的语料越多越好,而且要有足够的多样性,这样基于这些初始语料施展遗传算法,效果才会更加。go-fuzz会将一些语料持久化成文件放在corpus中,以供下次restart使用。

前面说过,go-fuzz是一个infinite loop,上面的测试需要手工停下来。go-fuzz会在workdir中创建另外两个目录:crashers和suppressions。顾名思义,crashers中存放的是代码crash时的相关数据,包括引起crash的case的输入二进制数据、输入的数据的字符串形式(xxx.quoted)以及基于这个数据的输出数据(xxx.output)。suppressions中保存着crash时的stack trace信息。

四、一个简单示例

gocmpp是一个cmpp协议库的go实现,这里打算用其中的unpack做一个最简单的fuzz test demo。

gocmpp中的每种协议包都实现了Packer接口,其中的Unpack尤其适合fuzz test。由于协议包众多,我们在gocmpp下专门建立fuzztest目录,用于存放fuzz test的代码,将各个协议包的fuzz test分到各个子目录中:

github.com/bigwhite/gocmpp/fuzztest]$tree
.
├── fwd
│   ├── corpus
│   │   └── 0
│   ├── fuzz.go
│   └── gen
│       └── main.go
└── submit
       ├── corpus
       │   ├── 0
       ├── fuzz.go
       └── gen
           └── main.go

先说说每个fuzz test单元(比如fwd或submit)下的gen/main.go,这是一个用于生成初始语料的可执行程序,我们以submit/gen/main.go为例:

package main

import (
    "github.com/dvyukov/go-fuzz/gen"
)

func main() {
    data := []byte{
        0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x02, 0x31, 0x33, 0x35, 0x30, 0x30, 0x30, 0x30, 0x32, 0x36, 0x39, 0x36, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x39, 0x30, 0x30, 0x30, 0x30,
        0x31, 0x30, 0x32, 0x31, 0x30, 0x00, 0x00, 0x00, 0x00, 0x31, 0x35, 0x31, 0x31, 0x30, 0x35, 0x31,
        0x33, 0x31, 0x35, 0x35, 0x35, 0x31, 0x30, 0x31, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x30, 0x30, 0x30, 0x30,
        0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x01, 0x31, 0x33, 0x35, 0x30, 0x30, 0x30, 0x30, 0x32, 0x36, 0x39, 0x36, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x6d, 0x4b, 0x8b, 0xd5, 0x00, 0x67, 0x00, 0x6f, 0x00,
        0x63, 0x00, 0x6d, 0x00, 0x70, 0x00, 0x70, 0x00, 0x20, 0x00, 0x73, 0x00, 0x75, 0x00, 0x62, 0x00,
        0x6d, 0x00, 0x69, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    }

    gen.Emit(data, nil, true)
}

在这个main.go中,我们借用submit包的单元测试中的数据作为fuzz test的初始语料数据,通过go-fuzz提供的gen包将数据输出到文件中:

$cd submit/gen
$go run main.go -out ../corpus/
$ll ../corpus/
total 8
drwxr-xr-x  3 tony  staff  102 12  7 22:00 ./
drwxr-xr-x  5 tony  staff  170 12  7 21:42 ../
-rw-r--r--  1 tony  staff  181 12  7 22:00 0

该程序在corpus下生成了一个文件“0”,作为submit fuzz test的初始语料。

接下来我们看看submit/fuzz.go:

// +build gofuzz

package cmppfuzz

import (
    "github.com/bigwhite/gocmpp"
)

func Fuzz(data []byte) int {
    p := &cmpp.Cmpp2SubmitReqPkt{}
    if err := p.Unpack(data); err != nil {
        return 0
    }
    return 1
}

这是一个“最简单”的Fuzz函数实现了,根据作者对Fuzz的规约,Fuzz的返回值是有重要含义的:

如果此次输入的数据在某种程度上是很有意义的,go-fuzz会给予这类输入更多的优先级,Fuzz应该返回1;
如果明确这些输入绝对不能放入corpus,那让Fuzz返回-1;
至于其他情况,返回0。

接下来就是go-fuzz-build和go-fuzz登场了,这与前面的介绍差不多:

$cd submit
$go-fuzz-build github.com/bigwhite/gocmpp/fuzztest/submit
$ls
cmppfuzz-fuzz.zip    corpus/            fuzz.go            gen/

在submit目录下执行go-fuzz:

$go-fuzz -bin=./cmppfuzz-fuzz.zip -workdir=./
2015/12/07 22:05:02 slaves: 4, corpus: 1 (3s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s
2015/12/07 22:05:05 slaves: 4, corpus: 3 (0s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 32, uptime: 6s
2015/12/07 22:05:08 slaves: 4, corpus: 7 (1s ago), crashers: 0, restarts: 1/5424, execs: 65098 (7231/sec), cover: 131, uptime: 9s
2015/12/07 22:05:11 slaves: 4, corpus: 9 (0s ago), crashers: 0, restarts: 1/5424, execs: 65098 (5424/sec), cover: 146, uptime: 12s
... ...
2015/12/07 22:09:11 slaves: 4, corpus: 9 (4m0s ago), crashers: 0, restarts: 1/9860, execs: 4033002 (16002/sec), cover: 146, uptime: 4m12s
^C2015/12/07 22:09:13 shutting down...

这个测试非常耗cpu啊!一小会儿功夫,我的Mac Air的风扇就开始呼呼转起来了。不过我的Unpack函数并未在fuzz test中发现问题,crashers后面的数值一直是0。

go-fuzz目前似乎还不支持vendor机制,因此如果你的包像gocmpp一样使用了vendor,那需要在go-fuzz-build和go-fuzz前面加上一个GO15VENDOREXPERIMENT=”0″(如果你之前开启了GO15VENDOREXPERIMENT),就像这样:

$ GO15VENDOREXPERIMENT="0" go-fuzz-build github.com/bigwhite/gocmpp/fuzztest/submit

如果不关闭vendor,你可能会得到类似如下的错误

can't find imported package golang.org/x/text/transform

Go语言错误处理

近期闲暇用Go写一个lib,其中涉及到error处理的地方让我琢磨了许久。关于Go错误处理的资料和视频已有许多,Go authors们也在官方Articles和Blog上多次提到过一些Go error handling方面的一些tips和best practice,这里仅仅算是做个收集和小结,尽视野所及,如有不足,欢迎评论中补充。(10月因各种原因,没有耕博,月末来一发,希望未为晚矣 ^_^)

一、概述

Go是一门simple language,常拿出来鼓吹的就是作为gopher习以为傲的仅仅25个关键字^_^。因此Go的错误处理也一如既往的简单。我们知道C语言错误处理以返 回错误码(errno)为主流,目前企业第一语言Java则用try-catch- finally的处理方式来统一应对错误和异常(开发人员常常因分不清楚到底哪些是错误,哪些是异常而滥用该机制)。Go则继承了C,以返回值为错误处理的主要方式(辅以panic与recover应对runtime异常)。但与C不同的是,在Go的惯用法中,返回值不是整型等常用返回值类型,而是用了一个 error(interface类型)。

type interface error {
    Error() string
}

这也体现了Go哲学中的“正交”理念:error context与error类型的分离。无论error context是int、float还是string或是其他,统统用error作为返回值类型即可。

func yourFunction(parametersList) (..., error)
func (Receiver)yourMethod(parametersList) (..., error)

在Andrew Gerrand的“Error handling and Go“一文中,这位Go authors之一明确了error context是由error接口实现者supply的。在Go标准库中,Go提供了两种创建一个实现了error interface的类型的变量实例的方法:errors.New和fmt.Errorf:

errors.New("your first error code")
fmt.Errorf("error value is %d\n", errcode)

这两个方法实际上返回的是同一个实现了error interface的类型实例,这个unexported类型就是errorString。顾名思义,这个error type仅提供了一个string的context!

//$GOROOT/srcerrors/errors.go

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

这两个方法也基本满足了大部分日常学习和开发中代码中的错误处理需求。

二、惯用法(idiomatic usage)

1、基本用法

就像上面函数或方法定义那样:

func yourFunction(parametersList) (..., error)
func (Receiver)yourMethod(parametersList) (..., error)

通常情况,我们将函数或方法定义中的最后一个返回值类型定义为error。使用该函数或方法时,通过如下方式判断错误码:

..., err := yourFunction(...)
if err != nil {
    //error handling
}

or

if ..., err := yourFunction(...); err != nil {
    //error handling
}

2、注意事项

1)、永远不要忽略(ignore)函数或方法返回的错误码,Check it。(例外:包括标准库在内的Go代码很少去判断fmt.Println or Printf系列函数的返回值)

2)、error的string context中的内容格式:头母小写,结尾不带标点。因为考虑到error被经常这么用:

... err := errors.New("error example")
fmt.Printf("The returned error is %s.\n", err)

3)、error处理流的缩进样式

prefer

..., err := yourFunction(...)
if err != nil {
    // handle error
}

//go on doing something.

rather than:

..., err := yourFunction(...)
if err == nil {
    // do something.
}

// handle error

三、槽点与破解之法

Go自诞生那天起就伴随着巨大争议,这也不奇怪,就像娱乐圈,如果没有争议,哪有存在感,刷脸的机会都没有。看来有争议是件好事,没争议的编程语言都已经成为了历史。炒作懂么!这也是很多Gopher的微博、微信、twitter、medium账号喜欢发“Why I do not like Go”类文章的原因吧^_^。

Go的error处理方式就是被诟病的点之一,反方主要论点就是Go的错误处理机制似乎回到了70年代(与C同龄^_^),使得错误处理代码冗长且重复(部分也是由于前面提到的:不要ignore任何一个错误码),比如一些常见的错误处理代码形式如下:

err := doStuff1()
if err != nil {
    //handle error...
}

err = doStuff2()
if err != nil {
    //handle error...
}

err = doStuff3()
if err != nil {
    //handle error...
}

这里不想去反驳这些论点,Go authors之一的Russ Cox对于这种观点进行过驳斥:当初选择返回值这种错误处理机制而不是try-catch这种机制,主要是考虑前者适用于大型软件,后者更适合小程序。当程序变大,try-catch会让错误处理更加冗长繁琐易出错(具体参见go faq)。不过Russ Cox也承认Go的错误处理机制对于开发人员的确有一定的心智负担。

好了,关于这个槽点的叙述点到为止,我们关心的是“如何破解”!Go的错误处理的确冗长,但使用一些tips,还是可以将代码缩减至可以忍受的范围的,这里列举三种:

1、checkError style

对于一些在error handle时可以选择goroutine exit(注意:如果仅存main goroutine一个goroutine,调用runtime.Goexit会导致program以crash形式退出)或os.Exit的情形,我们可以选择类似常见的checkError方式简化错误处理,例如:

func checkError(err error) {
    if err != nil {
        fmt.Println("Error is ", err)
        os.Exit(-1)
    }
}

func foo() {
    err := doStuff1()
    checkError(err)

    err = doStuff2()
    checkError(err)

    err = doStuff3()
    checkError(err)
}

这种方式有些类似于C中用宏(macro)简化错误处理过程代码,只是由于Go不支持宏,使得这种方式的应用范围有限。

2、聚合error handle functions

有些时候,我们会遇到这样的情况:

err := doStuff1()
if err != nil {
    //handle A
    //handle B
    ... ...
}

err = doStuff2()
if err != nil {
    //handle A
    //handle B
    ... ...
}

err = doStuff3()
if err != nil {
    //handle A
    //handle B
    ... ...
}

在每个错误处理过程,处理过程相似,都是handle A、handle B等,我们可以通过Go提供的defer + 闭包的方式,将handle A、handle B…聚合到一个defer匿名helper function中去:

func handleA() {
    fmt.Println("handle A")
}
func handleB() {
    fmt.Println("handle B")
}

func foo() {
    var err error
    defer func() {
        if err != nil {
            handleA()
            handleB()
        }
    }()

    err = doStuff1()
    if err != nil {
        return
    }

    err = doStuff2()
    if err != nil {
        return
    }

    err = doStuff3()
    if err != nil {
        return
    }
}

3、 将doStuff和error处理绑定

在Rob Pike的”Errors are values”一文中,Rob Pike told us 标准库中使用了一种简化错误处理代码的trick,bufio的Writer就使用了这个trick:

    b := bufio.NewWriter(fd)
    b.Write(p0[a:b])
    b.Write(p1[c:d])
    b.Write(p2[e:f])
    // and so on
    if b.Flush() != nil {
            return b.Flush()
        }
    }

我们看到代码中并没有判断三个b.Write的返回错误值,错误处理放在哪里了呢?我们打开一下$GOROOT/src/

type Writer struct {
    err error
    buf []byte
    n   int
    wr  io.Writer
}

func (b *Writer) Write(p []byte) (nn int, err error) {
    for len(p) > b.Available() && b.err == nil {
        ... ...
    }
    if b.err != nil {
        return nn, b.err
    }
    ......
    return nn, nil
}

我们可以看到,错误处理被绑定在Writer.Write的内部了,Writer定义中有一个err作为一个错误状态值,与Writer的实例绑定在了一起,并且在每次Write入口判断是否为!= nil。一旦!=nil,Write其实什么都没做就return了。

以上三种破解之法,各有各的适用场景,同样你也可以看出各有各的不足,没有普适之法。优化go错误处理之法也不会局限在上述三种情况,肯定会有更多的solution,比如代码生成,比如其他还待发掘。

四、解调用者之惑

前面举的例子对于调用者来讲都是较为简单的情况了。但实际编码中,调用者不仅要面对的是:

if err != nil {
    //handle error
}

还要面对:

if err 是 ErrXXX
    //handle errorXXX

if err 是 ErrYYY
    //handle errorYYY

if err 是ErrZZZ
    //handle errorZZZ

我们分三种情况来说明调用者该如何处理不同类型的error实现:

1、由errors.New或fmt.Errorf返回的错误变量

如果你调用的函数或方法返回的错误变量是调用errors.New或fmt.Errorf而创建的,由于errorString类型是unexported的,因此我们无法通过“相当判定”或type assertion、type switch来区分不同错误变量的值或类型,唯一的方法就是判断err.String()是否与某个错误context string相等,示意代码如下:

func openFile(name string) error {
    if file not exist {
        return errors.New("file does not exist")
    }

    if have no priviledge {
        return errors.New("no priviledge")
    }
    return nil
}

func main() {
    err := openFile("example.go")
    if err.Error() == "file does not exist" {
        // handle "file does not exist" error
        return
    }

    if err.Error() == "no priviledge" {
        // handle "no priviledge" error
        return
    }
}

但这种情况太low了,不建议这么做!一旦遇到类似情况,就要考虑通过下面方法对上述情况进行重构。

2、exported Error变量

打开$GOROOT/src/os/error.go,你会在文件开始处发现如下代码:

var (
    ErrInvalid    = errors.New("invalid argument")
    ErrPermission = errors.New("permission denied")
    ErrExist      = errors.New("file already exists")
    ErrNotExist   = errors.New("file does not exist")
)

这些就是os包export的错误码变量,由于是exported的,我们在调用os包函数返回后判断错误码时可以直接使用等于判定,比如:

err := os.XXX
if err == os.ErrInvalid {
    //handle invalid
}
... ...

也可以使用switch case:

switch err := os.XXX {
    case ErrInvalid:
        //handle invalid
    case ErrPermission:
        //handle no permission
    ... ...
}
... ...

(至于error类型变量与os.ErrInvalid的可比较性可参考go specs

一般对于库的设计和实现者而言,在库的设计时就要考虑好export出哪些错误变量。

3、定义自己的error接口实现类型

如果要提供额外的error context,我们可以定义自己的实现error接口的类型;如果这些类型还是exported的,我们就可以用type assertion or type switch来判断返回的错误码类型并予以对应处理。

比如$GOROOT/src/net/net.go:

type OpError struct {
    Op string
    Net string
    Source Addr
    Addr Addr
    Err error
}

func (e *OpError) Error() string {
    if e == nil {
        return "<nil>"
    }
    s := e.Op
    if e.Net != "" {
        s += " " + e.Net
    }
    if e.Source != nil {
        s += " " + e.Source.String()
    }
    if e.Addr != nil {
        if e.Source != nil {
            s += "->"
        } else {
            s += " "
        }
        s += e.Addr.String()
    }
    s += ": " + e.Err.Error()
    return s
}

net.OpError提供了丰富的error Context,不仅如此,它还实现了除Error以外的其他method,比如:Timeout(实现net.timeout interface) 和Temporary(实现net.temporary interface)。这样我们在处理error时,可通过type assertion或type switch将error转换为*net.OpError,并调用到Timeout或Temporary方法来实现一些特殊的判定。

err := net.XXX
if oe, ok := err.(*OpError); ok {
    if oe.Timeout() {
        //handle timeout...
    }
}

五、坑(s)

每种编程语言都有自己的专属坑(s),Go虽出身名门,但毕竟年轻,坑也不少,在error处理这块也可以列出几个。

1、 Go FAQ:Why is my nil error value not equal to nil?

type MyError string

func (e *MyError) Error() string {
    return string(*e)
}

var ErrBad = MyError("ErrBad")

func bad() bool {
    return false
}

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = &ErrBad
    }
    return p // Will always return a non-nil error.
}

func main() {
    err := returnsError()
    if err != nil {
        fmt.Println("return non-nil error")
        return
    }
    fmt.Println("return nil")
}

上面的输出结果是”return non-nil error”,也就是说returnsError返回后,err != nil。err是一个interface类型变量,其underlying有两部分组成:类型和值。只有这两部分都为nil时,err才为nil。但returnsError返回时将一个值为nil,但类型为*MyError的变量赋值为err,这样err就不为nil。解决方法:

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = &ErrBad
        return p
    }
    return nil
}

2、switch err.(type)的匹配次序

试想一下下面代码的输出结果:

type MyError string

func (e MyError) Error() string {
    return string(e)
}

func Foo() error {
    return MyError("foo error")
}

func main() {
    err := Foo()
    switch e := err.(type) {
    default:
        fmt.Println("default")
    case error:
        fmt.Println("found an error:", e)
    case MyError:
        fmt.Println("found MyError:", e)
    }
    return

}

你可能会以为会输出:”found MyError: foo error”,但实际输出却是:”found an error: foo error”,也就是说e先匹配到了error!如果我们调换一下次序呢:

... ...
func main() {
    err := Foo()
    switch e := err.(type) {
    default:
        fmt.Println("default")
    case MyError:
        fmt.Println("found MyError:", e)
    case error:
        fmt.Println("found an error:", e)
    }
    return
}

这回输出结果变成了:“found MyError: foo error”。

也许你会认为这不全是错误处理的坑,和switch case的匹配顺序有关,但不可否认的是有些人会这么去写代码,一旦这么写,坑就踩到了。因此对于通过switch case来判定error type的情况,将error这个“通用”类型放在后面或去掉。

六、第三方库

如果觉得go内置的错误机制不能很好的满足你的需求,本着“do not reinvent the wheel”的精神,建议使用一些第三方库来满足,比如:juju/errors。这里就不赘述了。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



Statcounter View My Stats