GOLANG错误处理最佳方案

GOLANG的错误很简单的,用error接口,参考[golang error handling](https://blog.golang.org/error-handling-and-go): ``` if f,err := os.Open("test.txt"); err != nil { return err } ``` 实际上如果习惯于C返回错误码,也是可以的,定义一个整形的error: ``` type errorCode int func (v errorCode) Error() string { return fmt.Sprintf("error code is %v", v) } const loadFailed errorCode = 100 func 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](https://github.com/pkg/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.New`和`errors.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 content main.load /Users/winlin/git/test/src/demo/test/main.go:160 main.initialize /Users/winlin/git/test/src/demo/test/main.go:167 main.main /Users/winlin/git/test/src/demo/test/main.go:179 runtime.main /usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185 runtime.goexit /usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197 load sys.db failed ``` 这样就可以知道是加载`sys.db`时候出错,错误内容是`empty content`,堆栈也有了。遇到错误时,会非常容易解决问题。 例如,AAC的一个库,用到了ASC对象,在解析时需要判断是否数据合法,实现如下(参考[code](https://github.com/ossrs/go-oryx-lib/blob/e482302c1c163934488a195765b5a239ea7eaa88/aac/aac.go#L348)): ``` 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 0x0 github.com/ossrs/go-oryx-lib/aac.(*AudioSpecificConfig).validate /Users/winlin/go/src/github.com/ossrs/go-oryx-lib/aac/aac.go:462 github.com/ossrs/go-oryx-lib/aac.(*adts).Decode /Users/winlin/go/src/github.com/ossrs/go-oryx-lib/aac/aac.go:439 main.run /Users/winlin/git/test/src/test/main.go:13 main.main /Users/winlin/git/test/src/test/main.go:19 runtime.main /usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185 runtime.goexit /usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197 adts decode ``` 错误信息包含: 1. `adts decode`,由ADTS打印出。 1. `invalid object 0x00`,由ASC打印出。 1. 完整的堆栈,包含`main/run/aac.Decode/asc.Decode`。 如果这个信息是客户端的,发送到后台后,非常容易找到问题所在,比一个简单的`Decode failed`有用太多了,有本质的区别。如果是服务器端,那还需要加上上下文关于连接的信息,区分出这个错误是哪个连接造成的,也非常容易找到问题。 加上堆栈会不会性能低?错误出现的概率还是比较小的,几乎不会对性能有损失。使用复杂的error对象,就可以在库中避免用logger,在应用层使用logger打印到文件或者网络中。 对于其他的语言,比如多线程程序,也可以用类似方法,返回int错误码,但是把上下文信息保存到线程的信息中,清理线程时也清理这个信息。对于协程也是一样的,例如[ST](https://github.com/ossrs/state-threads)的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++ style logger(int level, void* ctx, const char* fmt, ...) // GOLANG style logger(level:int, ctx:context.Context, format string, args ...interface{}) ``` 这样在文本日志,或者在消息系统中,就可以区分出哪个会话。当然在error中也可以包含context的信息,这样不仅仅可以看到出错的错误和堆栈,还可以看到之前的重要的日志。还可以记录线程信息,对于多线程和回调函数,可以记录堆栈: ``` [2017-06-08 09:44:10.815][Error][54417][100][60] Main: Run, code=1015 : run : callback : cycle : api=http://127.0.0.1:8080, url=rtmp://localhost/live/livestream, token=16357216378262183 : parse json={"code":0,"data":{"servers":["127.0.0.1:1935"]}} : no data.key thread #122848: run() [src/test/main.cpp:303][errno=60] thread #987592: do_callback() [src/test/main.cpp:346][errno=36] thread #987592: cycle() [src/sdk/test.cpp:3332][errno=36] thread #987592: do_cycle() [src/sdk/test.cpp:3355][errno=36] thread #987592: gslb() [src/sdk/test.cpp:2255][errno=36] thread #987592: gslb_parse() [src/sdk/test.cpp:2284][errno=36] ``` 当然,在ComplexError中得加入`uint64_t trd`和`int rerrno`,然后new和wrap时赋值就好了。

23 个评论

1. 对调用返回的用warp 2. 输出切记用%+v, 而不是用%v
不过针对recover的error如何正确能找到出错代码,有何见解呢
winlin

winlin 回复 tkk

1. 如果err已经Wrap过了,那就不用再Wrap一次,可以WithMessage,再Wrap一次只会加一个没有用的堆栈信息。 2. 对的,是用%+v,我改下,谢谢。
winlin

winlin 回复 tkk

recover一般是未知的错误,所以一般不可能panic(errors.New(xxx)),因为如果是已知的err那就应该return而不是panic。 如果是未知的错误,那么可以直接获取堆栈的。 万一别人panic(err),那也可以获取到这个err,可以定义一个局部的Cause接口检查下,可以参考:https://github.com/ossrs/go-oryx-lib/blob/e482302c1c163934488a195765b5a239ea7eaa88/errors/errors.go#L257
立哥, 那个c++的有库吗, 给个链接可好~
tkk

tkk 回复 winlin

未知的recover, 输出的信息只能到"最近的"一个方法,不能完整定位具体是具体是哪行代码出来的,比如最常见的, 空指针调用错误.
没有库,我对照golang的errors自己YY的,自己照这个写就好了,
winlin

winlin 回复 tkk

抱歉没有听懂,能否举个栗子?
...., 6666
tkk

tkk 回复 winlin

package main func main() { defer func() { if err := recover(); err != nil { //如果知道是methodB的错误 } }() methodA() } func methodA() { methodB() } type AA struct { p int } func (a *AA) t() { a.p = 1 } func methodB() { var _a *AA _a.t() }
winlin

winlin 回复 tkk

package main import ( "fmt" "runtime/debug" ) func main() { defer func() { if err := recover(); err != nil { fmt.Printf("%s: %s", err, debug.Stack()) } }() methodA() } func methodA() { methodB() } type AA struct { p int } func (a *AA) t() { a.p = 1 } func methodB() { var _a *AA _a.t() }
winlin

winlin 回复 tkk

Sorry, 貌似获取不到正确的堆栈。
winlin

winlin 回复 tkk

如果是主动panic的,是可以知道堆栈,debug.PrintStack()就可以: package main import ( "runtime/debug" ) func main() { defer func() { if err := recover(); err != nil { debug.PrintStack() } }() methodA() } func methodA() { methodB() } func methodB() { panic("error") }
谢大家的gocn评论不能用markdown,不能删除和修改,好蓝瘦啊。 我写了个简单的例子,直接对[]byte设置: ``` package main import ( "runtime/debug" ) func main() { defer func() { if err := recover(); err != nil { debug.PrintStack() } }() methodA() } func methodA() { methodB() } func methodB() { var b []byte b[0] = 0x00 } ```
主动的没用啊。。主要是未知的, 这块一直是头疼的问题
winlin

winlin 回复 tkk

如果调用methodB用defer调用的,可以看到B里面的栈,你可以试试: package main func main() { methodA() } func methodA() { defer methodB() } func methodB() { var b []byte b[0] = 0x00 } 另外,你知道是怎么回事吗?我找了google groups,没有找到答案。
还有个办法,在methodB加一个defer,就可以拿到堆栈了,你可以试试: ``` package main func main() { methodA() } func methodA() { methodB() } func methodB() { defer func(){}() var b []byte b[0] = 0x00 } ```
winlin

winlin 回复 tkk

我觉得是golang为了避免stack中有非常多的系统函数的堆栈信息, 所以系统panic时会找上一个用户的defer的位置,我发个消息问问是不是这么回事。
winlin

winlin 回复 tkk

发了一个文章问一下,找了google和groups都没有答案,看有没有人知道的:https://groups.google.com/d/msg/golang-nuts/clvdsdousDw/sHLSG7SnAAAJ
tkk

tkk 回复 winlin

赞一个,看了有人回复了, 期待1.9
winlin

winlin 回复 tkk

对的,是inling引起的,内链编译导致这几个函数堆一坨去了,你可以编译时加这个选项,就可以解决了。
winlin

winlin 回复 tkk

我又可以发一篇文章装逼了,哈哈。
tkk

tkk 回复 winlin

哈哈, 66666

要回复文章请先登录注册