Go で nil ポインタを入れた error が nil にならない理由
Go のインターフェース値は動的な型と動的な値を持ちます。nil ポインタを error として返すと err != nil になる理由を仕様で確かめ、安全な返し方と検査方法の限界を整理します。
関数は失敗していないのに、呼び出し側の if err != nil が真になることがあります。関数の中で *MyError 型の変数を作り、エラーがなければ nil のまま return したときです。ポインタは確かに nil なのに、error として受け取った値は nil ではありません。
Go FAQ が “Why is my nil error value not equal to nil?” という項目で取り上げている問題です。この記事では言語仕様と FAQ の規則だけを使ってこの挙動を説明し、どこまで避けられるかを整理します。基準にしたのは 2026-10-15 に確認した Go 言語仕様(Language version go1.27, May 26, 2026)です。ここで扱う規則は特定のバージョンで加わったものではなく、インターフェース値の基本的な定義から導かれます。
インターフェース値には型も入っている
仕様の “Variables” の節によると、インターフェース型の変数は二つの型を持ちます。宣言に書かれた静的な型と、実行時に代入された値の(インターフェースではない)型である動的な型です。代入された値があらかじめ宣言された識別子 nil であれば、動的な型はありません。nil には型がないからです。
仕様に載っている例がそのまま要点を示しています。
var x interface{} // x is nil and has static type interface{}
var v *T // v has value nil, static type *T
x = 42 // x has value 42 and dynamic type int
x = v // x has value (*T)(nil) and dynamic type *T
最後の行で v は nil ポインタですが、x には動的な型 *T と値 (*T)(nil) が一緒に入ります。
FAQ は同じ内容を実装寄りの図で説明しています。インターフェースは内部的に型 T と値 V の二つの要素からなり、インターフェース値が nil になるのは V と T がどちらも設定されていないときだけです。nil の *int をインターフェースに入れると、ポインタの値に関係なく内部の型は *int になるので(T=*int, V=nil)、そのインターフェース値は nil ではありません。この二要素のモデルは FAQ 自身が “schematically” と断っている説明用の図で、仕様が定めているのは動的な型と動的な値という概念です。
== nil の比較が実際に問うもの
仕様の “Comparison operators” の節では、二つのインターフェース値は動的な型が同一で動的な値が等しいとき、または両方の値が nil のときに等しいとされます。err == nil の右辺の nil は、動的な型を持たない nil インターフェース値として比較されます。そのため動的な型が *MyError で埋まっている err は、中のポインタが nil でも nil と等しくなりません。
return も同じ規則に従います。“Return statements” の節は、返す式が関数の結果型の対応する要素に代入可能でなければならないと定め、“Assignability” の節は、x がインターフェース T を実装していれば T に代入可能だと定めています。結果型が error の関数から *MyError の値を返すと、その値は動的な型 *MyError を持つ error の値になります。
問題が起きるコード
次の例は FAQ の returnsError の例と同じ構造です。Go 1.27 で実行すると false を出力したあと、runtime error: invalid memory address or nil pointer dereference というランタイムパニックで止まります。
package main
import "fmt"
type MyError struct {
Op string
}
func (e *MyError) Error() string {
return e.Op + " failed"
}
func find(ok bool) error {
var p *MyError
if !ok {
p = &MyError{Op: "find"}
}
return p // ok が true でも nil ではない error が返る
}
func main() {
err := find(true)
fmt.Println(err == nil) // false
fmt.Println(err.Error()) // run-time panic
}
find(true) は nil ポインタ p を返しますが、呼び出し側では err の動的な型が *MyError です。したがって err == nil は false です。
最後の行のパニックは原因を取り違えやすいところです。仕様の “Selectors” の節は、nil のインターフェース値でメソッドを呼ぶとランタイムパニックになると定めていますが、ここでの err は nil インターフェースではありません。(*MyError).Error は nil のレシーバで普通に呼ばれ、パニックはメソッド本体の e.Op で起きます。同じ節は、nil ポインタを通して構造体のフィールドを評価するとランタイムパニックになると定めています。スタックトレースも main.(*MyError).Error の return e.Op + " failed" の行を指しています。レシーバを参照外ししないメソッドなら、パニックせずに文字列を返すこともあり得ます。どちらにしても、呼び出し側は失敗していない関数を失敗したものとして扱ってしまいます。
名前付きの結果パラメータでも同じです。func find() (err error) の中で err = p として nil ポインタを代入してから return すると、代入の時点で err の動的な型が *MyError になります。
安全な返し方
FAQ が勧める解決策は、成功したときに nil を明示して返すことです。
func find(ok bool) error {
if !ok {
return &MyError{Op: "find"}
}
return nil
}
return nil の nil は結果型 error に代入されるときに nil インターフェース値になります。動的な型が埋まることはありません。
FAQ はさらに、エラーを返す関数のシグネチャには *MyError のような具象型ではなく error 型を使うよう勧めています。例として挙げられているのは os.Open です。この関数は nil でないときは常に具象型 *os.PathError の値を返しますが、シグネチャは error です。具象ポインタ型を返す関数の結果を、別の場所で error の変数に移した時点で同じ問題がまた起きるからです。
nil かもしれないポインタをすでに持っていて、それをインターフェースとして渡す必要があるなら、変換する前にポインタそのものを検査します。
func wrap(p *MyError) error {
if p == nil {
return nil
}
return p
}
p == nil はポインタ同士の比較で、仕様上二つのポインタ値は両方が nil なら等しくなります。インターフェースに変わる前に検査するので、見たとおりに動きます。
受け取る側での検査とその限界
nil ポインタを入れたインターフェース値をすでに受け取ってしまった場合、仕様だけでできる検査はどれも具象型がわかっていることが前提です。
- 具象型の nil と比較する: 仕様では、インターフェースでない型
Xの値xとインターフェース値tを比較でき、tの動的な型がXと同一で動的な値がxと等しければ両者は等しいとされます。そのため上の例ではerr == (*MyError)(nil)がtrueになります。 - comma-ok 形式の型アサーション:
p, ok := err.(*MyError)は、動的な型が*MyErrorならokがtrueになり、pには格納されているポインタが入ります。続けてp == nilでポインタを検査できます。アサーションが成り立たなければokはfalse、pは*MyErrorのゼロ値で、パニックは起きません。 - 型スイッチの
case nil: 仕様によると、case nilが選ばれるのはスイッチの対象が nil インターフェース値のときだけです。動的な型が*MyErrorの値はcase nilではなくcase *MyErrorに進みます。
どの方法も対象の型を一つずつ書く必要があります。「どのポインタ型であれ nil を入れているか」を一般的に問う演算子は仕様にありません。この記事は仕様と FAQ の範囲に限っているので、reflect パッケージを使う検査は扱いません。実際には、受け取る側で取り除くよりも、送る側で nil ポインタがインターフェースに入らないようにするほうが確実です。
まとめ
- インターフェース値は動的な型と動的な値を持ち、どちらもないときだけ nil です。
- nil ポインタをインターフェースに代入したり返したりすると動的な型が埋まるので、
== nilはfalseになります。 - 成功時は
return nilを明示し、エラーを返す関数のシグネチャはerrorにします。 - nil かもしれないポインタは、インターフェースに変わる前に検査します。変わった後は、この記事で見た比較、型アサーション、型スイッチでは具象型を書かなければ見つけられません。
FAQ の最後の一文が要点を短くまとめています。インターフェースに何らかの具象値が格納されていれば、そのインターフェースは nil ではありません。
出典
- The Go Programming Language Specification(Language version go1.27, May 26, 2026)、2026-10-15 確認: https://go.dev/ref/spec
- Go FAQ, “Why is my nil error value not equal to nil?”、2026-10-15 確認: https://go.dev/doc/faq