問題
以下のコードを実行するとどうなるか?
package main
import "fmt"
type unit struct{}
func (u *unit) String() string { return "UNIT" }
func null() *unit { return nil }
func main() {
var s fmt.Stringer = null()
if s != nil {
fmt.Println(s.String())
}
}
- コンパイルエラー
- 実行時panic
-
UNIT
が出力される - 何も起こらない
解答
解答
解説
本問題のテーマは**「Goのヤヤコシイnilの挙動」**です。要点は次の2つです。
- ポインタのnil値は「参照ができない」だけ
- インタフェースのnil値とポインタのnil値は違う
ポインタのnil値
ポインタのnil値は「参照先が存在しない」ことを示すものであるため、参照先取得(dereference)はできない(panicが発生する)のは当然です。注意すべきなのは**「参照先取得が起こらない限りはnil値の操作は問題がない」**ということです。
本問題では、ポインタのnil値をもつ変数x
についてのセレクタ式x.f
の解釈が問題になっています。これについて言語規格では以下のように定めています。
If
x
is of pointer type and has the valuenil
andx.f
denotes a struct field, assigning to or evaluatingx.f
causes a run-time panic.
([Selectors][selectors])
もし「x
が構造体のポインタでありf
がその構造体のフィールドを指す」のであれば、x.f
は実質的に(*x).f
と等価であり、そのフィールドへのアクセスは「x
の参照先取得」を要するためpanicとなります。
しかし、f
がメソッドを指す場合は、x.f
という式そのものはx
の参照先取得を伴いません。またx.f(…)
のようにメソッドを呼び出す場合についても、(ポインタレシーバである場合は)レシーバのx
は単に引数として関数に渡されるだけなのでやはりx
の参照先取得は起こりません。つまり、x.f(…)
は無問題です。
※もしメソッドx.f
が値レシーバである場合は、関数に渡されるのは参照先取得した*x
となるため、x.f(…)
はpanicになります。
インタフェースのnil値
明確な記述がなくてアレなんですが、言語規格において、「インタフェースのnil値1」は「ポインタのnil値」とは(また「その他の種類」の型のnil値」とも)区別されています。
Variables of interface type also have a distinct dynamic type, which is the concrete type of the value assigned to the variable at run time (unless the value is the predeclared identifier nil, which has no type). The dynamic type may vary during execution but values stored in interface variables are always assignable to the static type of the variable.
([Variables][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
1行目は「x
がインタフェースのnil値」をもつ場合で、これについては「x
はnilである」と書かれています。これに対して「x
が*T
型のポインタのnil値」をもつ4行目では「x
は値(*T)(nil)
をもち動的型*T
をもつ」と書かれています。先の引用文にある規定より「x
がnilであるならば動的型をもたない」はずなので、結局この場合は「x
はnilではない」と解釈するしかありません。つまり、言語規格では、「インタフェース型の変数x
がポインタのnil値(*T)(nil)
をもつ」場合は「x
はnil値をもたない」(あるいは「x
はnilではない」)と扱われるのです。要するに**「インタフェース型の話をするときにはポインタのnil値はnilではない」**のです。
詳細
以上の要点を踏まえてmain()
の実行を追ってみます。
var s fmt.Stringer = null()
null()
は*unit
型のnil値を返します。上の文は以下のものと同値です。
var s fmt.Stringer = (*unit)(nil)
つまりs
には*unit
型の「ポインタのnil値」が入ります。
if s != nil {
インタフェース型の等価比較は次のように決められています。
Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value
nil
.
([Comparison operators][comparison])
つまり「①両者が同一の動的型と等価な動的値をもつ」または「②両者がnil値をもつ」場合です。従って、s == nil
の比較では両方ともnilであるため②に該当する……のではありません! 先ほど述べた通り、「インタフェース型の話をするときにはポインタのnil値はnilではない」ので左辺のs
は「nilではない」ことになります。(右辺のnil
は「インタフェースのnil値」なので「nilである」ことになります。)従って②は成立しません。①については、右辺のnil
(インタフェースのnil値)は動的型をもたないので成立しません。結果、s == nil
の等価比較は偽になります。
従って、if s != nil {…}
の条件は成立することになるためブロックの中が実行されます。
fmt.Println(s.String())
s.String()
というインタフェースのメソッド呼出について考えてみます。インタフェースに対するセレクタ式について次の規定があります。
If
x
is of interface type and has the valuenil
, calling or evaluating the methodx.f
causes a run-time panic.
([Selectors][selectors])
しかしこれまで散々述べた通り、「s
はnilではない」のでこれは適用されません。
For a value
x
of typeI
whereI
is an interface type,x.f
denotes the actual method with namef
of the dynamic value ofx
. If there is no method with namef
in the method set ofI
, the selector expression is illegal.
([Selectors][selectors])
s
の動的型は*unit
なので、この規定に従い、s.String()
は結局(*unit)(s).String()
、つまりは(*unit)(nil).String()
に帰着されます。unitには実際にポインタレシーバのString
メソッドが存在します。
func (u *unit) String() string { return "UNIT" }
レシーバの値はポインタのnil値ですが、先に述べた通り何も問題はなく、このString
メソッドがu
をnilとして呼ばれます。関数定義の中でもu
は一度も参照されていないため、何の問題も起こらず、"UNIT"
が返ってきます。
すなわち、s.String()
の値は"UNIT"
であるため、UNIT
が出力されてプログラムは終了します。
まとめ
- ポインタのnil値は「参照ができない」だけ
- インタフェースのnil値とポインタのnil値は違う
Goのnilはヤヤコシイ
-
「nilインタフェース値(nil interface value)」という語は規格書の中で1度だけ登場します。
[selectors]: https://golang.org/ref/spec#Selectors
[variables]: https://golang.org/ref/spec#Variables
[comparison]: https://golang.org/ref/spec#Comparison_operators ↩