GOLANG错误处理最佳方案

来源:互联网 发布:oracle数据库日志清理 编辑:程序博客网 时间:2024/06/06 02:20

原文:https://gocn.io/article/348

GOLANG的错误很简单的,用error接口,参考golang error handling:

if f,err := os.Open("test.txt"); err != nil {    return err}

实际上如果习惯于C返回错误码,也是可以的,定义一个整形的error:

type errorCode intfunc (v errorCode) Error() string {    return fmt.Sprintf("error code is %v", v)}const loadFailed errorCode = 100func load(filename string) error {    if f,err := os.Open(filename); err != nil {        return loadFailed    }    defer f.Close()    content : = readFromFile(f);    if len(content) == 0 {        return loadFailed    }    return nil}

这貌似没有什么难的啊?实际上,这只是error的基本单元,在实际的产品中,比如有个播放器会打印一个这个信息:

Player: Decode failed.

对的,就只有这一条信息,然后呢?就没有然后了,只知道是解码失败了,没有任何的线索,必须得调试播放器才能知道发生了什么。看我们的例子,如果load失败,也是一样的,只会打印一条信息:

error code is 100

这些信息是不够的,这是一个错误库很流行的原因,这个库是errors,它提供了一个Wrap方法:

_, err := ioutil.ReadAll(r)if err != nil {        return errors.Wrap(err, "read failed")}

也就是加入了多个error,如果用这个库,那么上面的例子该这么写:

func load(filename string) error {    if f,err := os.Open(filename); err != nil {        return errors.Wrap(err, "open failed")    }    defer f.Close()    content : = readFromFile(f);    if len(content) == 0 {        return errors.New("content empty")    }    return nil}

这个库给每个error可以加上额外的消息errors.WithMessage(err,msg),或者加上堆栈信息errors.WithStack(err),或者两个都加上erros.Wrap, 或者创建带堆栈信息的错误errors.Newerrors.Errorf。这样在多层函数调用时,就有足够的信息可以展现当时的情况了。

在多层函数调用中,甚至可以每层都加上自己的信息,例如:

func initialize() error {    if err := load("sys.db"); err != nil {        return errors.WithMessage(err, "init failed")    }    if f,err := os.Open("sys.log"); err != nil {        return errors.Wrap(err, "open log failed")    }    return nil}

init函数中,调用load时因为这个err已经被Wrap过了,所以就只是加上自己的信息(如果用Wrap会导致重复的堆栈,不过也没有啥问题的了)。第二个错误用Wrap加上信息。打印日志如下:

empty contentmain.load    /Users/winlin/git/test/src/demo/test/main.go:160main.initialize    /Users/winlin/git/test/src/demo/test/main.go:167main.main    /Users/winlin/git/test/src/demo/test/main.go:179runtime.main    /usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185runtime.goexit    /usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197load sys.db failed

这样就可以知道是加载sys.db时候出错,错误内容是empty content,堆栈也有了。遇到错误时,会非常容易解决问题。

例如,AAC的一个库,用到了ASC对象,在解析时需要判断是否数据合法,实现如下(参考code):

func (v *adts) Decode(data []byte) (raw, left []byte, err error) {    p := data    if len(p) <= 7 {        return nil, nil, errors.Errorf("requires 7+ but only %v bytes", len(p))    }    // Decode the ADTS.    if err = v.asc.validate(); err != nil {        return nil, nil, errors.WithMessage(err, "adts decode")    }    return}func (v *AudioSpecificConfig) validate() (err error) {    if v.Channels < ChannelMono || v.Channels > Channel7_1 {        return errors.Errorf("invalid channels %#x", uint8(v.Channels))    }    return}

在错误发生的最原始处,加上堆栈,在外层加上额外的必要信息,这样在使用时发生错误后,可以知道问题在哪里,写一个实例程序:

func run() {    adts,_ := aac.NewADTS()    if _,_,err := adts.Decode(nil); err != nil {        fmt.Println(fmt.Sprintf("Decode failed, err is %+v", err))    }}func main() {    run()}

打印详细的堆栈:

Decode failed, err is invalid object 0x0github.com/ossrs/go-oryx-lib/aac.(*AudioSpecificConfig).validate    /Users/winlin/go/src/github.com/ossrs/go-oryx-lib/aac/aac.go:462github.com/ossrs/go-oryx-lib/aac.(*adts).Decode    /Users/winlin/go/src/github.com/ossrs/go-oryx-lib/aac/aac.go:439main.run    /Users/winlin/git/test/src/test/main.go:13main.main    /Users/winlin/git/test/src/test/main.go:19runtime.main    /usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185runtime.goexit    /usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197adts decode

错误信息包含:

  1. adts decode,由ADTS打印出。
  2. invalid object 0x00,由ASC打印出。
  3. 完整的堆栈,包含main/run/aac.Decode/asc.Decode

如果这个信息是客户端的,发送到后台后,非常容易找到问题所在,比一个简单的Decode failed有用太多了,有本质的区别。如果是服务器端,那还需要加上上下文关于连接的信息,区分出这个错误是哪个连接造成的,也非常容易找到问题。

加上堆栈会不会性能低?错误出现的概率还是比较小的,几乎不会对性能有损失。使用复杂的error对象,就可以在库中避免用logger,在应用层使用logger打印到文件或者网络中。

对于其他的语言,比如多线程程序,也可以用类似方法,返回int错误码,但是把上下文信息保存到线程的信息中,清理线程时也清理这个信息。对于协程也是一样的,例如ST的thread也可以拿到当前的ID,利用全局变量保存信息。对于goroutine这种拿不到协程ID,可以用context.Context,实际上最简单的就是在error中加入上下文,因为Context要在1.7之后才纳入标准库。

一个C++的例子,得借助于宏定义:

struct ComplexError {    int code;    ComplexError* wrapped;    string msg;    string func;    string file;    int line;};#define errors_new(code, fmt, ...) \    _errors_new(__FUNCTION__, __FILE__, __LINE__, code, fmt, ##__VA_ARGS__)extern ComplexError* _errors_new(const char* func, const char* file, int line, int code, const char* fmt, ...) {    va_list ap;    va_start(ap, fmt);    char buffer[1024];    size_t size = vsnprintf(buffer, sizeof(buffer), fmt, ap);    va_end(ap);    ComplexError* err = new ComplexError();    err->code = code;    err->func = func;    err->file = file;    err->line = line;    err->msg.assign(buffer, size);    return err;}#define errors_wrap(err, fmt, ...) \    _errors_wrap(__FUNCTION__, __FILE__, __LINE__, err, fmt, ##__VA_ARGS__)extern ComplexError* _errors_wrap(const char* func, const char* file, int line, ComplexError* v, const char* fmt, ...) {    ComplexError* wrapped = (ComplexError*)v;    va_list ap;    va_start(ap, fmt);    char buffer[1024];    size_t size = vsnprintf(buffer, sizeof(buffer), fmt, ap);    va_end(ap);    ComplexError* err = new ComplexError();    err->wrapped = wrapped;    err->code = wrapped->code;    err->func = func;    err->file = file;    err->line = line;    err->msg.assign(buffer, size);    return err;}

使用时,和GOLANG有点类似:

ComplexError* loads(string filename) {    if (filename.empty()) {        return errors_new(100, "invalid file");    }    return NULL;}ComplexError* initialize() {    string filename = "sys.db";    ComplexError* err = loads(filename);    if (err) {        return errors_wrap("load system from %s failed", filename.c_str());    }    return NULL;}int main(int argc, char** argv) {    ComplexError* err = initialize();    // Print err stack.    return err;}

比单纯一个code要好很多,错误发生的概率也不高,获取详细的信息比较好。

另外,logger和error是两个不同的概念,比如对于library,错误时用errors返回复杂的错误,包含丰富的信息,但是logger一样非常重要,比如对于某些特定的信息,access log能看到客户端的访问信息,还有协议一般会在关键的流程点加日志,说明目前的运行状况,此外,还可以有json格式的日志或者叫做消息,可以把这些日志发送到数据系统处理。

对于logger,支持context.Context就尤其重要了,实际上context就是一次会话比如一个http request的请求的处理过程,或者一个RTMP的连接的处理。一个典型的logger的定义应该是:

// C++ stylelogger(int level, void* ctx, const char* fmt, ...)// GOLANG stylelogger(level:int, ctx:context.Context, format string, args ...interface{})

这样在文本日志,或者在消息系统中,就可以区分出哪个会话。当然在error中也可以包含context的信息,这样不仅仅可以看到出错的错误和堆栈,还可以看到之前的重要的日志。