GOLANGエラー処理最適案

14987 ワード

原文: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 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が失敗した場合も同様で、1つの情報しか印刷されません.
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を使うと重複するスタックになりますが、問題はありません).2番目のエラーは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を使用することができます.実際に最も簡単なのは、Contextが1.7以降に標準ライブラリに組み込まれるため、errorにコンテキストを追加することです.
    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は2つの異なる概念であり、例えば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の情報も含まれていて、エラーのエラーやスタックだけでなく、前の重要なログも見ることができます.