Goのエラーハンドリングの思想についての自由研究
Goのエラーハンドリングの思想についての日本語の記事が少なく、理解ができてない。
例えば以下の疑問に答えられるようにしたい。
- なぜ
if err != nil { return err }
を書き続ける必要があるのか? - なぜ直和型のResultを採用しなかったのか?
- なぜGoにはスタックトレースがないのか?
開発者やその周辺のエラーハンドリングに関する記事やコメントを探っていくことで、エラーハンドリングの思想について理解を深めていく。
主に以下のやりとりを見ていこうと思う。
Rob Pike先生
「Errors are values」とのこと
- エラーハンドリングは別に
if err != nil { return err }
という書き方にこだわる必要はない - エラーはプログラム上ではただの値なのだから、バッファしてから一括処理したりなど、色々工夫して良い
例:
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
return ew.err
}
Go 2への、pass
キーワードの提案
f, err := os.Open(filename)
pass err
defer f.Close()
- エラーハンドリングの記述が楽になるという主張
-
pass
キーワードはエラーがnilでない場合にのみ関数からのリターンを行うため、コードをスキャンする際にerr == nil
とerr != nil
を混同しない
・互換性が高い
「err == nil
とerr != nil
を混同しない」というのはなるほどと思った。コメントでも一定評価は受けてる。
しかし、どうせやるならreturn if
とかreturn?
にすべきではという意見が優勢か。
日本語で簡単にまとまってる記事
tryキーワードの提案。
f := try(os.Open(filename))
2019のgopherconでの資料らしい。
ここでもConceptは「Errors are values」としており、スタックトレースに頼るよりもfmt.Errorf
でラップした方が良いと主張。
スタックトレースに関して、「Stacktraces are for disasters」と言っており、以下の主張をしている。
-
main.go:10
と書いてあっても、エラー箇所がわかるだけでエラー原因が分からないじゃん! - 読みにくいし、解析しにくい
例えば以下のように書きなさいと言ってる。
return fmt.Errorf("unique error message: %w", err)
また、errorに以下のような構造体を持たせ、自分でカスタマイズもできる。
・Level
によって重大度ごとに分類できる。
・Kind
を定義すれば、タイプ別に分類できる。
・Op
を定義すれば、スタックトレースの代わりになる
こちらはRob Pike先生の記事。
というか上記のKind
, Op
などを使ったパターンは、この記事のパターンにインスピレーションを受けている。
type Error struct {
Path upspin.PathName
User upspin.UserName
Op Op
Kind Kind
Err error
}
func E(args ...interface{}) error
- エラーは価値ある情報: エラーは単なる障害ではなく、発生した問題に関する価値ある情報を提供するもの。
- エラーの文脈: エラーを伝播する際には、発生した文脈を付与する
- エラーの一貫性: プロジェクト全体でエラーの取り扱いに一貫性を持たせる。
- ユーザーにとって有益なエラー: エラーは開発者だけでなく、エンドユーザーにとっても有益な情報を提供し、ユーザーがエラーの原因を理解し、可能な場合は問題を解決できるようにすることが望ましい。
- エラー処理はアート: 過度に複雑にすることなく、適切な量の情報を提供すること。
Russ Cox先生による記事。
Go 2ではエラーハンドリングをもう少し簡素にしたいとのこと。
Russ Cox先生による記事。
Go 2ではエラーハンドリングをもう少し簡素にしたいとのこと。
Draft案(あくまで議論のための足掛かりとしている)として、check
とhandle
をという予約語を用意し、エラーcheckで失敗すると、制御は上にある次のhandle
へと移る。
func CopyFile(src, dst string) error {
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
handle err {
w.Close()
os.Remove(dst) // (only if a check fails)
}
check io.Copy(w, r)
check w.Close()
return nil
}
try
でなくcheck
にしたのは、以下の例で不自然なため。
data, err := parseHexdump(string(hex))
if err == ErrBadHex {
... special handling ...
}
try err
また、RustではResult
型とパターンマッチが存在し、これはしばしば冗長な記述になってしまうとしている。
?
演算子は冗長な記述は避けられるが、上記のhandle
のような(例えばerror
をラップできるような)仕組みではないと。
fmt.Fprintln
のように実は失敗しうる関数は、エラーを無視できないような書き方に強制すべきという提案。
つまり、以下のように書くように強制する。
_, _ = fmt.Fprintln(&buf, v)
また、同時にエラーを無視できるignoreという予約語も提案している。
go ignore(fmt.Fprintln(os.Stderr, findTheBug())) // BUG: evaluated immediately
反対意見は以下。
- hello worldの出力で
_, _ =
と書く必要があるのは、初学者にとって気持ち悪い - どちらかというと
vet
の機能では