nil ポインタエラー
導入
あるコード作成中に、複数のオブジェクトを閉じるために Close() メソッドを呼び出す必要がありました。以下のコードのようになります。
type A struct {
b B
c C
d D
}
func (a A) Close() error {
if a.b != nil {
if err := a.b.Close(); err != nil {
return err
}
}
if a.c != nil {
if err := a.c.Close(); err != nil {
return err
}
}
if a.d != nil {
if err := a.d.Close(); err != nil {
return err
}
}
return nil
}しかし、これほど多くの if 判断を書くのはエレガントではないと感じました。B、C、D はすべて Close メソッドを実装しているので、もっと簡潔にできるはずです。そこで、それらを 1 つのスライスに入れてループで判断するようにしました。
func (a A) Close() error {
closers := []io.Closer{
a.b,
a.c,
a.d,
}
for _, closer := range closers {
if closer != nil {
if err := closer.Close(); err != nil {
return err
}
}
}
return nil
}これで見ればもっと良く見えます。では実行してみましょう。
func main() {
var a A
if err := a.Close(); err != nil {
panic(err)
}
fmt.Println("success")
}結果は予想外で、なんとクラッシュしました。エラー情報は以下の通りで、nil レシーバーに対してメソッドを呼び出すことはできないという意味です。ループ内の if closer != nil がフィルタリング機能を果たしていないようです。
panic: value method main.B.Close called using nil *B pointer上記の例は私が以前に遭遇したバグの簡略化版です。多くの初心者は最初は私と同じような間違いを犯すかもしれません。以下で一体どういうことなのかを説明します。
インターフェース
以前の章で述べたように、nil は参照型のゼロ値です。例えば、スライス、map、チャネル、関数、ポインタ、インターフェースのゼロ値です。スライス、map、チャネル、関数については、それらをすべてポインタと見なすことができ、ポインタが具体的な実装を指しています。

しかし、インターフェースだけが異なります。インターフェースは 2 つのもので構成されています:型と値です。

変数に nil を代入しようとすると、コンパイルに失敗し、以下の情報が表示されます。
use of untyped nil in assignment内容は概ね、値が untyped nil の変数を宣言できないというものです。untyped nil があれば、相対的に typed nil も存在するはずです。このような状況はしばしばインターフェースで発生します。以下の簡単な例をご覧ください。
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(pa)
fmt.Println(pa == nil)
}出力結果:
<nil>
true
<nil>
false結果は非常に奇妙いです。明らかに pa の出力は nil ですが、nil と等しくありません。リフレクションを使用してそれが一体何なのかを見てみましょう。
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.TypeOf(pa))
fmt.Println(reflect.ValueOf(pa))
}出力結果:
<nil>
true
*int
<nil>結果から、実際には (*int)(nil) であることがわかります。つまり、pa に格納されている型は *int で、実際の値は nil です。インターフェース型の値に対して等値演算を行う際、まず型が等しいかどうかを判断し、型が等しくなければ直接等しくないと判定され、次に値が等しいかどうかを判断します。この部分のインターフェース判断ロジックは cmd/compile/internal/walk.walkCompare 関数を参照できます。
したがって、インターフェースを nil に等しくしたい場合、その値が nil であり、かつ型も nil である必要があります。インターフェース内の型は実際にはポインタだからです。
type iface struct {
tab *itab
data unsafe.Pointer
}型を迂回して直接値が nil かどうかを判断したい場合、リフレクションを使用できます。以下はその例です。
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.ValueOf(pa).IsNil())
}IsNil() を使用して直接値が nil かどうかを判断できます。これにより、上記の問題が発生しなくなります。したがって、通常の使用过程中に、関数の戻り値がインターフェース型である場合、ゼロ値を返したい場合は、直接 nil を返すのが最善です。具体的な実装のゼロ値を返さないでください。それがインターフェースを実装していても、決して nil と等しくなりません。これにより、例のエラーが発生する可能性があります。
まとめ
上記の問題を解決したので、次に以下の例を見てみましょう。
構造体のレシーバーがポインタレシーバーの場合、nil は使用可能です。以下の例をご覧ください。
type A struct {
}
func (a *A) Do() {
}
func main() {
var a *A
a.Do()
}このコードは正常に実行され、空ポインタエラーは発生しません。
スライスが nil の場合、その長さと容量にアクセスでき、要素を追加することもできます。
func main() {
var s []int
fmt.Println(len(s))
fmt.Println(cap(s))
s = append(s, 1)
}map が nil の場合、それにアクセスできますが、nil の map は読み取り専用です。書き込みを試みると panic が発生します。
func main() {
var s map[string]int
i, ok := s[""]
fmt.Println(i, ok)
fmt.Println(len(s))
// 書き込みを試みると、panic が発生します
s["a"] = 1 // panic: assignment to entry in nil map
}上記の例の nil に関するこれらの特性は混乱を招く可能性があります。特に Go の初心者にとって、nil は上記のいくつかの型のゼロ値、つまりデフォルト値を表します。デフォルト値はデフォルトの動作を示すべきです。これも Go の設計者が意図したことです:nil をより有用にし、空ポインタエラーを直接スローしないようにすることです。この理念は標準ライブラリにも反映されています。例えば、HTTP サーバーを起動するには次のように書けます。
http.ListenAndServe(":8080", nil)nil Handler を直接渡すことができ、http ライブラリはデフォルトの Handler を使用して HTTP リクエストを処理します。
TIP
興味のある方はこちらの動画をご覧ください Understanding nil - Gopher Conference 2016。非常に明確でわかりやすく説明されています。
