Goのinterfaceのnilについて

interfaceのnilチェックをしているのにメソッド呼び出し時にpanicが起きることがあり、その時に調べたことのメモです。

該当コードを簡略化して書くと以下のような感じです。

package main

import "fmt"

type Message interface {
	Str() string
}

var _ Message = &Text{}

type Text struct {
	text string
}

func (t *Text) Str() string {
	return t.text
}

func NewText(str string) *Text {
	if str == "" {
		return nil
	}
	return &Text{
		text: str,
	}
}

NewText() は引数が空文字の場合にnilを返します。

これに対して、以下のような Print() 関数を用意して実行すると、

func Print(m Message) {
	if m != nil {
		fmt.Println(m.Str())
	} else {
		fmt.Println("nil Message")
	}
}

func main() {
	Print(NewText("hello"))
	Print(NewText(""))
}

これはpanicになります。

$ go run main.go 
hello
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x47e260]

goroutine 1 [running]:
main.(*Text).Str(0x4b1960)
        /app/main.go:16
main.Print({0x4b1940, 0x0})
        /app/main.go:30 +0x2e
main.main()
        /app/main.go:40 +0x54
exit status 2

m.Str() の前で m 自体を出力すると <nil> が表示されます。

func Print(m Message) {
	if m != nil {
		fmt.Println(m)          // 追加
		fmt.Println(m.Str())
	} else {
		fmt.Println("nil Message")
	}
}
$ go run main.go 
&{hello}
hello
<nil>
panic: runtime error: invalid memory address or nil pointer dereference
...

調べてみると以下の記事を見つけました。

interfaceはtypeとvalueをという2つの要素を持っていて、両方nilの場合に interface == nil がtrueとみなされるようです。

自分のコードでは NewText() の返り値で *Text を指定しているので、関数でnilを返してもtypeは *Text になっていて m != nil がtrueになりメソッドが呼ばれていた、ということでした。

func NewText(str string) *Text {
	if str == "" {
		return nil
	}
	return &text{
		text: str,
	}
}

NewText() の返り値を Message interfaceにすると期待通りの動きをします。

func NewText(str string) Message {
	if str == "" {
		return nil
	}
	return &text{
		text: str,
	}
}
$ go run main.go 
hello
nil Message

普段から雰囲気でコードを書いているとこういう時にハマるので気をつけようと思いました。