emahiro/b.log

日々の勉強の記録とか育児の記録とか。

メソッドを持つ参照型をフィールドに持つ Struct の初期化後のアクセスについて

Overview

メソッドを持つ参照型をフィールドを持つカスタム struct を初期化したときの参照型が持つ(見かけ上ネストしてる)メソッドにアクセスした時の振る舞いがたまたま職場の雑談で上がり、挙動について調べてみたのその備忘録。

Sample

type Y struct{}

func (*Y) Hello() {
    fmt.Println("Hello")
}

type X struct {
    y *Y
}

X は Hello というメソッドを持つ Y の参照型をフィールドに持つカスタム struct です。この Y を明示的に初期化せず X を初期化したときに Hello にアクセスするケースを考えます。

func NewX() *X {
    return &X{}
}

func main() {
    x := NewX()
    x.y.Hello()

}

ref: https://play.golang.org/p/lPsjP1ZcIe1

このとき Y は明示的に初期化されていない (= &Y{} みたいなケース)ので、 X の持つ y property には nil が入りますが、Hello メソッドは問題なく Call できて Hello という文字列が出力されます。
Go をある程度書き慣れていると、y にアクセスした時点で nilpo で panic が発生しそうなコードに思えますが、これで panic が発生することはありません。
これは振る舞いとしてはX が初期化されたタイミングで、Y は参照型なのでその実体は nil であるもの Hello メソッドは Y の参照型に対する メソッドなので nil でも呼べる、というものになります。
たとえフィールド自体が nil でも Y の参照型として実体 nil なのであって、参照型が持っているメソッド自体は nilpo にならずに Call できる、というものです。書き慣れてきたからこそ分かりづらい仕様です。

参考までに以下のような nil の中身に触るコードは nilpo が発生します。

package main

import (
    "fmt"
)

type Name struct{}

type Y struct {
    nameStruct *Name
}

func (y *Y) getNameStruct() *Name {
    return y.nameStruct
}

func (y *Y) Hello() {
    fmt.Printf("%v\n", y.getNameStruct())
}

type X struct {
    y *Y
}

func NewX() *X {
    return &X{}
}

func main() {
    x := NewX()
    x.y.Hello()

}

ref: https://play.golang.org/p/mTF8GPEmEWO

これは nil である変数の中身に触っているからです。上記のような挙動を確認するためには以下のようなコードで nilpo が起きないことでさらに確認ができます。

package main

import (
    "fmt"
)

type Name struct{}

type Y struct {
    nameStruct *Name
}

func (y *Y) getNameStruct() *Name {
    return &Name{}
}

func (y *Y) Hello() {
    fmt.Printf("%v\n", y.getNameStruct())
}

type X struct {
    y *Y
}

func NewX() *X {
    return &X{}
}

func main() {
    x := NewX()
    fmt.Printf("%#v\n", x)
    x.y.Hello()

}

ref: https://play.golang.org/p/7Ca2MXCx8Pb

このケースでは getNameStruct メソッドの中で y の実体(nil) に触ってないので nilpo は発生しません。
ちなみにアドレスを確認しても nil を挿してます。 ref: https://play.golang.org/p/d1HE52yuavk

まとめ

たまに Go だとこの振る舞いどうだっけー?っていうのを調べると面白いですが、今回紹介したようなコードは Go に慣れると混乱しかねない仕様だなと思ったので、struct を初期化するときには明示的に nil や参照型の struct を指定してあげると良いのではないかなと思いました。nil でも呼べるけど意図しない nil だった、みたいなことがないように、という意図です。

func NewX() *X {
    return &X{
        y: nil,
    }
}

// or 

func NewX() *X {
    return &X{
        y: &Y{},
    }
}