自己紹介
澤田
2018年新卒入社。新卒研修を経てAmeba事業本部に配属され、Amebaブログのバックエンド開発に従事。好きな言語はGoで、最近は静的解析に興味を持っています。
小野
2021新卒入社のバックエンドエンジニアです。現在はAmeba事業本部にてブログ刷新プロジェクトの開発に携わっています。
この記事について
この記事は、先日行われたgolang.tokyo #32で発表した wrapmsg というlinterの話をもうちょっと深掘りする記事になります。
当日の資料はこちらです。wrapmsg — fmt.Errorf のメッセージに制約を —
golang.tokyo #32は「自作ライブラリ、ソフトウェアGo自慢大会」というテーマで開催されました。作った物を発表する場があんまりないよね、ということで開催されています。
TL; DR
wrapmsg は fmt.Errorf でエラーを wrap するときのメッセージに制限をかけるlinterです。
準標準ライブラリのwrap箇所を参考に、エラーを返した呼び出しの情報を最低限だけ含める形式へと制限をかけます。
singlechecker や multichecker を用いてバイナリをつくると –fix オプションにて自動fixをすることも可能になっています。
課題感
fmt.Errorf でエラーの wrap をする際に、「失敗した」ことを示すために「failed 〜〜」や「cannot 〜〜」といったメッセージを書いていました。
その結果、呼び出しもとでエラーを表示してみると「failed 〜〜: cannot 〜〜: cannot 〜〜」というように「失敗した」という情報が冗長になっていました。また、記述する人によって表記の揺れがあり、時によっては文法的に誤った英語が記述されていることもありました。
実は「失敗した」という情報は型が error であることからわかるのでメッセージ中には含める必要がありません。
そこで、Goにおけるベストプラクティスを探るべくGoの標準や準標準と言われるパッケージについて調査することに決めました。
調査したこと
調査ではgithub.com/golang/goを含むgithub.com/golangにあるリポジトリのコードを対象に”fmt.Errorf”で検索をかけてwrap箇所を見ていきました。
するとgithub.com/golang/pkgsite や https://github.com/golang/build などで課題を解決できそうなメッセージのフォーマットでwrapしている箇所をいくつか見つけました。
下のコード例のようにwrapするerrorを返したメソッドや関数の名前をそのままメッセージに使うような形式です。
cfg, err := pgx.ParseConfig(conn) if err != nil { return fmt.Errorf("pgx.ParseConfig() = %w", err) }
bytes, err := ioutil.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("ioutil.ReadAll: %v", err) }
作ったコード規約の詳細
調査を踏まえて、fmt.Errorfでエラーのwrapをする際はそのエラーを返した呼び出しをそのまま記述することとしました。例えば、 err := foo.Bar.Baz().Hoge(fuga)
であれば、 fmt.Errorf(“foo.Bar.Baz.Huga: %w”)
といった具合です。
こうすることで、「何に」失敗したのかという情報を簡潔に含めることができます。また、私たちが書いているのはライブラリではなくアプリケーションなので、エラーの調査をする際にはコードを管理している自分達で行うことになります。もちろんコード内を検索してたどっていくことは可能ですので、このメッセージをトレース情報のように扱うことができます。
linterを作る上で苦労したこと
静的単一代入 (Single Static Assign; SSA) 形式でエラーの呼び出し元を見つけられるのではないかと思い実装を始めましたが、SSA形式を扱うのが初めてだったこともあり情報をどうたどっていけばいいかに苦労しました。
Goの準標準ライブラリが提供するSSA形式はグラフ構造となっています。グラフ構造をたどる時には再帰呼び出しをうまく使うとシンプルになるのですが、各フィールドにどういった型の値が入っているかを調べながら実装する必要がありました。
最終的にどういった手順で実装したかというと…
- まず、linterに期待することをテストケースとしてわかる範囲で全て起こしました。
- 次に、再帰関数中のtype switchにて未知の型が来たらログを出しつつエラーを返すようにしました
- テストが通るようになるまでtype switchの型とそれに対する実装を足していきました
という感じの実装方法になりました。
linterの紹介
完成したのは github.com/Warashi/wrapmsg
という linter です。
go install github.com/Warashi/wrapmsg/cmd/wrapmsg
して go vet
に vettool として渡してもいいですし、 singlechecker や multichecker を使って単体のバイナリをビルドすることもできます。
単体のバイナリをビルドした際には --fix
オプションを渡してやることで自動fixをすることもできるようになっています。
エラーをwrapするときのメッセージに悩んでいるそこのあなた、ぜひ使ってみてください!