GOLANG錯誤處理最佳方案

winlin發表於2017-06-05

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.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 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

錯誤資訊包含:

  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++ 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 trdint rerrno,然後 new 和 wrap 時賦值就好了。

更多原創文章乾貨分享,請關注公眾號
  • GOLANG錯誤處理最佳方案
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章