はじめに

今回は go 言語の型付 nil の直感的でない挙動について説明します。 まずは、次のコードを実行してみてください。 playground

package main

import "fmt"

func main() {
	var x *int
	var y *int = nil
	var z error

	fmt.Printf("x -> %#10v; x is nil -> %v\n", x, x == nil)
	fmt.Printf("y -> %#10v; y is nil -> %v\n", y, y == nil)
	fmt.Printf("z -> %#10v; z is nil -> %v\n", z, z == nil)
	fmt.Printf("x isNil  -> %v\n", isNil("x", x))
	fmt.Printf("y isNil  -> %v\n", isNil("y", y))
	fmt.Printf("z isNil  -> %v\n", isNil("z", z))
}

func isNil(name string, x interface{}) bool {
	fmt.Printf("%s -> %#10v; ", name, x)
	return x == nil
}

上のコードを実行結果は以下のようになります。
実行結果より、関数の引数を interface{}として受け取った際にx == nilが型情報を持った nil の場合はイコールが true にならないことが分かります。

x -> (*int)(nil); x is nil -> true
y -> (*int)(nil); y is nil -> true
z ->      <nil>; z is nil -> true
x -> (*int)(nil); x isNil  -> false
y -> (*int)(nil); y isNil  -> false
z ->      <nil>; z isNil  -> true

コード上ではどちらも同じ (*int)(nil) であるのに、方やx==nil -> trueとなり、方やx==nil -> falseとなっています。とても不思議に見えます。
今回はなぜこのような挙動になるのかを見ていきます。

メモリの中身を確認

さきほどのコードを変更し、変数のメモリの中身を直接見てみます。playground

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var x *int
	var y *int = nil
	var z error

	fmt.Printf("byte(%v); x is nil -> %v\n", *(*[4]byte)(unsafe.Pointer(&x)), x == nil)
	fmt.Printf("byte(%v); y is nil -> %v\n", *(*[4]byte)(unsafe.Pointer(&y)), y == nil)
	fmt.Printf("byte(%v); z is nil -> %v\n", *(*[4]byte)(unsafe.Pointer(&z)), z == nil)
	fmt.Printf("x isNil  -> %v\n", isNil(x))
	fmt.Printf("y isNil  -> %v\n", isNil(y))
	fmt.Printf("z isNil  -> %v\n", isNil(z))
}

func isNil(x interface{}) bool {
	fmt.Printf("byte(%v); ", *(*[4]byte)(unsafe.Pointer(&x)))
	return x == nil
}

出力結果は以下のようになります。出力結果より、interface 化された値が本当は nil じゃないことがわかります。
(byte([0 0 0 0])が nil ということです。)

byte([0 0 0 0]); x is nil -> true
byte([0 0 0 0]); y is nil -> true
byte([0 0 0 0]); z is nil -> true
byte([128 22 74 0]); x isNil  -> false
byte([128 22 74 0]); y isNil  -> false
byte([0 0 0 0]); z isNil  -> true

それでは、なぜ interface 化すると、nil でなくなってしまうのでしょうか?

interface の構造

interface の中身を図示すると以下のような図になります。

itable

interface は図のように2つのデータを持っています。

  1. 型情報(+interface で定義されたメソッド)
  2. 実際のデータへのアドレス(値がポインタより小さい場合は、直接保持している)

そのため、interface が持っているのは実際の値だけでなく、型の情報も含めて持っているということになります。これが最初の挙動の正体です。
つまり、interface 化された際に型の情報が保存されたため、アドレスが何も指していない状態==nilではなくなってしまい、nil とのイコールが成り立たなくなってしまった、ということです。すごいトラップです。

まとめ

型の情報を持った nil を interface として関数の引数にしたため、型情報をがアドレス空間に保存されてしまい、nil でなくなってしまう、ということでした。
また、interface の nil(最初の例で言えば error 型)で宣言した場合は、そもそも型情報が欠落しているため前述の挙動にならず純粋な nil のままとなります。その場合は、直感的な挙動になるため、もしも自前の error 型を作成する際などは今回のことを念頭に置いておき、型付 nil を作成しないように注意することが大切です。

参考文献