GOLANG錯誤處理最佳方案
GOLANG 的錯誤很簡單的,用 error 介面,參考golang error handling:
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,它提供了一個 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):
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
錯誤資訊包含:
-
adts decode
,由 ADTS 列印出。 -
invalid object 0x00
,由 ASC 列印出。 - 完整的堆疊,包含
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++ 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 時賦值就好了。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Golang通脈之錯誤處理Golang
- 【GoLang】GoLang 錯誤處理 -- 官方推薦方式 示例Golang
- Go語言(golang)的錯誤(error)處理的推薦方案GolangError
- Golang 學習——error 錯誤處理淺談GolangError
- JavaScript 錯誤處理的最佳實踐JavaScript
- [譯] NodeJS 錯誤處理最佳實踐NodeJS
- 「Golang成長之路」錯誤處理與資源管理Golang
- 錯誤處理
- 如何優雅的在Golang中進行錯誤處理Golang
- 「Golang成長之路」錯誤處理與資源管理篇Golang
- PHP 錯誤處理PHP
- php錯誤處理PHP
- Go 錯誤處理Go
- Swift錯誤處理Swift
- Zabbix錯誤處理
- mysqldump錯誤處理MySql
- 錯誤處理:如何通過 error、deferred、panic 等處理錯誤?Error
- PHP錯誤處理和異常處理PHP
- go的錯誤處理Go
- Python錯誤處理Python
- 【GoLang 那點事】深入 Go 的錯誤處理機制 (一) 使用Golang
- Golang錯誤處理函式defer、panic、recover、errors.New介紹Golang函式Error
- 異常錯誤資訊處理
- PHP 核心特性 - 錯誤處理PHP
- 常用模組 PHP 錯誤處理PHP
- laravel9 錯誤處理Laravel
- 淺談前端錯誤處理前端
- Oracle異常錯誤處理Oracle
- ORACLE 異常錯誤處理Oracle
- 15-錯誤處理(Error)Error
- 學習Rust 錯誤處理Rust
- axios 的錯誤處理iOS
- Go語言之錯誤處理Go
- Objective-C:錯誤處理Object
- javascript之處理Ajax錯誤JavaScript
- 搭建dataguard時,錯誤處理
- Oracle錯誤處理思路(一)Oracle
- COM的錯誤處理 (轉)