Errorx-Goでエラーを処理するためのライブラリ

Errorxとは何か、どのように役立つか



Errorxは、Goでエラーを処理するためのライブラリです。 大規模プロジェクトのエラーメカニズムに関連する問題を解決するためのツールと、それらを操作するための単一の構文を提供します。







画像







ほとんどのJoomサーバーコンポーネントは、会社設立以来Goで作成されています。 この選択は、開発の初期段階とサービスの存続期間で報われました。Go2の見通しに関する発表を踏まえると、今後も後悔することはないでしょう。 Goの主な長所の1つは単純さであり、エラーへのアプローチは、この原則を他に類を見ないほど実証しています。 すべてのプロジェクトが十分な規模に達していないため、標準ライブラリの機能が十分ではないため、この分野で独自のソリューションを探す必要があります。 エラーを処理するためのアプローチがたまたま進化し、errorxライブラリはこの進化の結果を反映しています。 私たちは、それが彼らのプロジェクトのエラーを扱うことにまだ大きな不快感を感じていない人々を含む多くの人々に役立つことができると確信しています。







Goの間違い



errorxに関する話に移る前に、いくつかの説明をする必要があります。 最後に、バグの何が問題になっていますか?







type error interface { Error() string }
      
      





とても簡単ですね 実際には、多くの場合、実装は実際にはエラーの文字列記述以外の情報を持ちません。 このようなミニマリズムは、ミスが必ずしも「例外的」なものを意味するわけではないアプローチに関連しています。 最も一般的に使用されるエラー。標準ライブラリのNew()は、この考えに当てはまります。







 func New(text string) error { return &errorString{text} }
      
      





言語のエラーに特別なステータスはなく、通常のオブジェクトであることを思い出すと、問題が発生します。エラーを処理することの特性は何ですか?







間違いも例外ではありません 。 多くの人がGoに慣れると、抵抗に抵抗してこの違いに遭遇するのは秘密ではありません。 Goで選択されたアプローチを説明し、サポートし、批判する多くの出版物があります。 いずれにせよ、Goのエラーには多くの目的があり、そのうちの少なくとも1つは他の言語の例外とまったく同じです。トラブルシューティングです。 その結果、それらの使用に関連するアプローチと構文が非常に異なっていても、同じ表現力を期待するのは自然です。







何が悪いの?



多くのプロジェクトは、そのままGoのバグを利用しており、これについて少しも困難はありません。 ただし、システムの複雑さが増すにつれて、高い期待がなくても注意を引く多くの問題が現れ始めます。 良い例は、サービスのログの同様の行です:







Error: duplicate key









ここで、最初の問題はすぐに明らかになります。これを意図的に処理しないと、何らかの大規模なシステムでは、最初のメッセージだけでは何が問題なのかを理解することはほとんど不可能です。 この投稿には詳細と問題のより広い文脈が欠けています。 これはプログラマーの間違いですが、無視できないほど頻繁に起こります。 制御グラフの「ポジティブ」ブランチ専用のコードは、実際には常に注意を払う必要があり、実行の中断や外部の問題に関連する「ネガティブ」コードよりもテストでカバーされます。 Goプログラムでif err != nil {return err}



マントラが繰り返される頻度はif err != nil {return err}



この見落としをさらに可能にします。







小さな余談として、この例を考えてみましょう。







 func (m *Manager) ApplyToUsers(action func(User) (*Data, error), ids []UserID) error { users, err := m.LoadUsers(ids) if err != nil { return err } var actionData []*Data for _, user := range users { data, err := action(user) if err != nil { return err } ok, err := m.validateData(data) if err != nil { return nil } if !ok { log.Error("Validation failed for %v", data) continue } actionData = append(actionData, data) } return m.Apply(actionData) }
      
      





このコードにエラーが表示されるのはどのくらいの速さですか? しかし、おそらくGoプログラマーによって少なくとも1回は行われました。 ヒント: if err != nil { return nil }



、式のエラー。







ログに不明瞭なメッセージが表示されて問題に戻ると、もちろん、誰もがこのような状況に陥っています。 問題発生時にすでにエラー処理コードの修正を開始するのは非常に不快です。 さらに、ログの初期データによると、どちらの側がコードのその部分の検索を開始するかは完全に不明であり、実際には改善が必要です。 これは、コードが小さく、外部依存関係の数が少ないプロジェクトでは、非常に複雑なように思えるかもしれません。 ただし、大規模プロジェクトの場合、これは完全に現実的で苦痛な問題です。







苦い経験のあるプログラマーが、返されるエラーに事前にコンテキストを追加したいとします。 これを行う単純な方法は、次のようなものです。







 func InsertUser(u *User) error { err := usersTable.Insert(u) if err != nil { return errors.New(fmt.Sprintf("failed to insert user %s: %v", u.Name, err) } return nil }
      
      





良くなった。 より広いコンテキストはまだ不明ですが、少なくともどのコードでエラーが発生したかを見つけるのがはるかに簡単になりました。 ただし、1つの問題を解決したため、別の問題をうっかり作成してしまいました。 ここで作成されたエラーにより、診断メッセージは元のままになりましたが、そのタイプや追加コンテンツを含む他のすべては失われました。







これが危険な理由を確認するには、データベースドライバーで同様のコードを検討してください。







 var ErrDuplicateKey = errors.New("duplicate key") func (t *Table) Insert(entity interface{}) error { // returns ErrDuplicateKey if a unique constraint is violated by insert } func IsDuplicateKeyError(err error) bool { return err == ErrDuplicateKey }
      
      





IsDuplicateKeyError()



チェックは破棄されますが、テキストをエラーに追加した時点では、そのセマンティクスを変更するつもりはありませんでした。 これにより、このチェックに依存するコードが破損します。







 func RegisterUser(u *User) error { err := InsertUser(u) if db.IsDuplicateKeyError(err) { // find existing user, handle conflict } else { return err } }
      
      





より賢くして独自の種類のエラーを追加し、元のエラーを保存して、たとえばCause() error



メソッドを介して返すことができるようにしたい場合、問題を部分的にしか解決しません。







  1. エラー処理の代わりに、真の理由がCause()



    ことを知る必要がありますCause()



  2. 外部ライブラリにこの知識を教える方法はなく、そこに書かれたヘルパー関数は役に立たないままです。
  3. 私たちの実装は、 Cause()



    エラーの直接の原因Cause()



    返すことを期待できます(そうでない場合はnil)。 標準ツールの欠如または一般に受け入れられている契約が非常に不快な驚きを脅かす


ただし、この部分的な解決策は、ある程度まで含めて、多くのエラーライブラリで使用されます。 Go 2には、このアプローチを普及させる計画があります。これが起こると、上記の問題に対処するのが簡単になります。







Errorx



以下では、errorxが提供するものについて説明しますが、最初にライブラリの基礎となる考慮事項を定式化してください。









私たちにとって最も難しい質問は拡張性でした。errorxは、動作が任意に異なるカスタムタイプのエラーを導入するためのプリミティブを提供する必要がありますか、または必要なものすべてをすぐに使用できる実装がありますか? 2番目のオプションを選択しました。 まず、errorxは非常に実用的な問題を解決します。それを使用した経験から、この目的のためには、ソリューションを作成するためのスペアパーツではなく、ソリューションを持つ方が良いことがわかります。 第二に、シンプルさを考慮することは非常に重要です。エラーに注意が払われないため、エラーを扱う際のバグがより困難になるようにコードを設計する必要があります。 このためには、このようなコードがすべて同じように見え、同じように動作することが重要であることを実践が示しています。







TL;メインライブラリ機能によるDR:





はじめに



errorxを使用して上記で分析した例を修正すると、次のようになります。







 var ( DBErrors = errorx.NewNamespace("db") ErrDuplicateKey = DBErrors.NewType("duplicate_key") ) func (t *Table) Insert(entity interface{}) error { // ... return ErrDuplicateKey.New("violated constraint %s", details) } func IsDuplicateKeyError(err error) bool { return errorx.IsOfType(err, ErrDuplicateKey) }
      
      





 func InsertUser(u *User) error { err := usersTable.Insert(u) if err != nil { return errorx.Decorate(err, "failed to insert user %s", u.Name) } return nil }
      
      





IsDuplicateKeyError()



を使用した呼び出し元コードは変更されません。







この例では何が変更されましたか?









常にそのようなスキームに従う必要はありません:









Godocには、これらすべてに関する詳細な情報が含まれています。 以下では、日常の作業に十分な主な機能についてもう少し詳しく説明します。







種類



errorxエラーは何らかのタイプに属します。 タイプが重要です 継承されたエラープロパティが渡される場合があります。 必要に応じてセマンティクステストが行​​われるのは、彼または彼の特性を介してです。 さらに、タイプの表現力豊かな名前はエラーメッセージを補足し、場合によってはそれを置き換えることがあります。







 AuthErrors = errorx.NewNamespace("auth") ErrInvalidToken = AuthErrors.NewType("invalid_token")
      
      





 return ErrInvalidToken.NewWithNoMessage()
      
      





エラーメッセージにはauth.invalid_token



が含まれauth.invalid_token



。 エラー宣言は異なるように見える場合があります。







 ErrInvalidToken = AuthErrors.NewType("invalid_token").ApplyModifiers(errorx.TypeModifierOmitStackTrace)
      
      





この実施形態では、タイプ修飾子を使用して、スタックトレース収集が無効にされる。 エラーにはマーカーセマンティクスがあります。そのタイプはサービスの外部ユーザーに与えられ、ログ内の呼び出しスタックは役に立たないでしょう。 これは修復する問題ではありません。







ここでは、エラーのいくつかの面でエラーが二重の性質を持つことを予約できます。 エラーの内容は、診断のほか、外部ユーザー(APIクライアント、ライブラリユーザーなど)の情報としても使用されます。 エラーは、発生した内容のセマンティクスを伝える手段として、および制御を移すためのメカニズムとして、コード内で使用されます。 エラータイプを使用する場合、これを覚えておく必要があります。







エラー作成



 return MyType.New("fail")
      
      





エラーごとに独自の型を取得することは完全にオプションです。 すべてのプロジェクトは、汎用エラーの独自のパッケージを持つことができ、一部のセットは、errorxとともに共通の名前空間の一部として提供されます。 ほとんどの場合、コードでの処理を伴わないエラーが含まれており、何か問題が発生した場合の「例外的な」状況に適しています。







 return errorx.IllegalArgument.New("negative value %d", value)
      
      





一般的な場合、呼び出しのチェーンは、エラーがチェーンの最後に作成され、最初に処理されるように設計されています。 Goでは、エラーを2回処理するのが悪い形式と見なされる理由がないわけではありません。たとえば、エラーをログに書き込み、スタックの上位にそれを返します。 ただし、エラーを提供する前に、エラー自体に情報を追加できます。







 return errorx.Decorate(err, "failed to upload '%s' to '%s'", filename, location)
      
      





エラーに追加されたテキストはログに表示されますが、元のエラーの種類を確認しても問題はありません。







時々、逆のニーズが発生します。エラーの性質が何であれ、パッケージの外部ユーザーはそれを知るべきではありません。 そのような機会があれば、実装の一部に脆弱な依存関係を作成できます。







 return service.ErrBadRequest.Wrap(err, "failed to load user data")
      
      





WrapがNewの代替として好ましい重要な違いは、元のエラーがログに完全に反映されることです。 そして、特に、有用な初期呼び出しスタックをもたらします。







呼び出しスタックに関するすべての可能な情報を保存できる別の便利なトリックは、次のようになります。







 return errorx.EnhanceStackTrace(err, "operation fail")
      
      





元のエラーが別のゴルーチンから発生した場合、そのような呼び出しの結果には、両方のゴルーチンのスタックトレースが含まれ、その有用性が異常に増加します。 このような課題を作成する必要があるのは明らかにパフォーマンスの問題によるものです。このケースは比較的まれであり、それ自体を検出する人間工学が通常のラップを遅くします。







Godocには詳細な情報が含まれており、DecorateManyなどの追加機能についても説明しています。







エラー処理



何よりも、エラー処理が次のようになる場合:







 log.Error("Error: %+v", err)
      
      





プロジェクトのシステム層のログにエラーを出力することを除いて、作成する必要のあるエラーが少ないほど良いです。 現実には、これでは十分でない場合があり、これを行う必要があります。







 if errorx.IsOfType(err, MyType) { /* handle */ }
      
      





このチェックは、タイプMyType



エラーとその子タイプの両方で成功し、 errorx.Decorate()



耐性があります。 ただし、ここでは、エラーのタイプに直接依存しています。これはパッケージ内では非常に正常ですが、外部で使用すると不快になる場合があります。 場合によっては、このようなエラーのタイプは安定した外部APIの一部であり、このチェックをエラーの正確なタイプではなくプロパティのチェックに置き換えたい場合があります。







古典的なGoエラーでは、これは、エラーのタイプの指標として機能する型キャストインターフェイスを通じて行われます。 Errorxタイプはこの拡張機能をサポートしていませんが、代わりにTrait



メカニズムを使用できます。 例:







 func IsTemporary(err error) bool { return HasTrait(err, Temporary()) }
      
      





errorxに組み込まれたこの関数は、エラーに標準プロパティTemporary



があるかどうかをチェックします。 一時的なものかどうか。 エラーのタイプを特性でマークすることは、エラーの原因の責任であり、それらを通して、特定の内部タイプを外部APIの一部にすることなく、有用なシグナルを送信できます。







 return errorx.IgnoreWithTrait(err, errorx.NotFound())
      
      





この構文は、制御フローを中断するために特定の種類のエラーが必要な場合に便利ですが、呼び出し側の関数に渡すべきではありません。







すべてがここにリストされているわけではありませんが、処理ツールは豊富にありますが、エラーの処理はできる限り単純なままにしておく必要があることを覚えておくことが重要です。 私たちが順守しようとしているルールの例:









外部errorx



ここでは、ボックスからライブラリユーザーが利用できるものについて説明しましたが、Joomではエラー関連コードの浸透が非常に大きいです。 ロギングモジュールは、署名のエラーを明示的に受け入れ、不正なフォーマットの可能性を排除するためにそれ自体を印刷し、エラーチェーンからオプションで利用可能なコンテキスト情報を抽出します。 goroutinを使用したパニックセーフな処理を行うモジュールは、パニックが発生した場合にエラーをアンパックし、元のスタックトレースを失わずにエラー構文を使用してパニックを表示する方法も知っています。 これのいくつかは、おそらく私たちも公開します。







互換性の問題



errorxを使用してエラーを処理できることに非常に満足しているという事実にもかかわらず、このトピック専用のライブラリコードの状況は理想とはほど遠いものです。 Joomでは、errorxの特定の実用的な問題を解決していますが、Goエコシステムの観点からは、これらすべてのツールセットを標準ライブラリに含めることが望ましいでしょう。 エラーは、そのソースが実際にまたは潜在的に別のパラダイムに属しているため、エイリアンと見なされる必要があります。 プロジェクトで受け入れられている形式で情報を運んでいない可能性があります。







ただし、他の既存のソリューションと競合しないように、いくつかのことが行われています。







形式'%+v'



、スタックトレース(存在する場合)とともにエラーを出力するために使用されます。 これはGoエコシステムの事実上の標準であり、Go 2のドラフトデザインにも含まれています。







Cause() error



errorx , , , Causer, errorx Wrap().







未来



, Go 2, . .







, errorx Go 1. , Go 2, . , , errorx.







Check-handle , errorx , a Unwrap() error



Wrap()



errorx (.. , , Wrap



), . , , .







design draft Go 2, errorx.Is()



errorx.As()



, errors .







おわりに



, , , - , . , API : , , . 1.0 , Joom. , - .







: https://github.com/joomcode/errorx







, !







画像








All Articles