Go: nil ポインタを入れた error は nil ではない(成功時は return nil と書く)
error などのインターフェース値は「型」と「値」の組。*MyErr 型の nil ポインタを error として返すと、型の情報が残るので err != nil が true になる。
何が起きるか
go
type MyErr struct{ msg string }
func (e *MyErr) Error() string { return e.msg }
func load() error {
var e *MyErr // nil ポインタ
// ... 失敗したときだけ e = &MyErr{...} にするつもり
return e // ← 成功時もここを通ると「(*MyErr, nil)」という error になる
}
func main() {
if err := load(); err != nil {
fmt.Println("失敗扱いになる:", err) // 成功のはずなのにここに入る
}
}インターフェース値が nil と等しいのは「型も値も空」のときだけ。return e で *MyErr という型が入るため、中身が nil でも != nil になる。呼び出し側で err.Error() を呼ぶと、メソッドによっては nil ポインタ参照で panic する。
直し方
- 成功時は型付きの変数ではなく、リテラルの
nilを返す。gofunc load() error { if bad { return &MyErr{msg: "bad"} } return nil } - 関数の戻り値の型は具体型(
*MyErr)ではなくerrorにする。具体型で返して呼び出し側でerror変数に代入すると、同じ問題が起きる。 - 途中でどうしても具体型の変数を使うなら、返す直前に
if e == nil { return nil }と分岐する。
見つけ方
- staticcheck の SA4023 は「インターフェース値と nil の比較が常に true / false になる」ケースを指摘してくれる(すべての経路を見つけられるわけではない)。
- 同じ落とし穴は
error以外のインターフェース(io.Readerなど)でも起きる。