LoginSignup
8
4

More than 3 years have passed since last update.

Goのnilに関するヤヤコシイ話

Last updated at Posted at 2020-12-05

問題

以下のコードを実行するとどうなるか?

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())
    }
}
  1. コンパイルエラー
  2. 実行時panic
  3. UNITが出力される
  4. 何も起こらない

scthinkingtime.png

解答


解答

3. UNITが出力される

解説

本問題のテーマは「Goのヤヤコシイnilの挙動」です。要点は次の2つです。

  1. ポインタのnil値は「参照ができない」だけ
  2. インタフェースのnil値とポインタのnil値は違う

ポインタのnil値

ポインタのnil値は「参照先が存在しない」ことを示すものであるため、参照先取得(dereference)はできない(panicが発生する)のは当然です。注意すべきなのは「参照先取得が起こらない限りはnil値の操作は問題がない」ということです。

本問題では、ポインタのnil値をもつ変数xについてのセレクタ式x.fの解釈が問題になっています。これについて言語規格では以下のように定めています。

If x is of pointer type and has the value nil and x.f denotes a struct field, assigning to or evaluating x.f causes a run-time panic.
(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)

ここでは、インタフェースの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()の実行を追ってみます。

1行目
    var s fmt.Stringer = null()

null()*unit型のnil値を返します。上の文は以下のものと同値です。

var s fmt.Stringer = (*unit)(nil)

つまりsには*unit型の「ポインタのnil値」が入ります。

2行目
    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)

つまり「①両者が同一の動的型と等価な動的値をもつ」または「②両者がnil値をもつ」場合です。従って、s == nilの比較では両方ともnilであるため②に該当する……のではありません! 先ほど述べた通り、「インタフェース型の話をするときにはポインタのnil値はnilではない」ので左辺のsは「nilではない」ことになります。(右辺のnilは「インタフェースのnil値」なので「nilである」ことになります。)従って②は成立しません。①については、右辺のnil(インタフェースのnil値)は動的型をもたないので成立しません。結果、s == nilの等価比較は偽になります

従って、if s != nil {…}の条件は成立することになるためブロックの中が実行されます。

3行目
        fmt.Println(s.String())

s.String()というインタフェースのメソッド呼出について考えてみます。インタフェースに対するセレクタ式について次の規定があります。

If x is of interface type and has the value nil, calling or evaluating the method x.f causes a run-time panic.
(Selectors)

しかしこれまで散々述べた通り、「snilではない」のでこれは適用されません。

For a value x of type I where I is an interface type, x.f denotes the actual method with name f of the dynamic value of x. If there is no method with name f in the method set of I, the selector expression is illegal.
(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が出力されてプログラムは終了します。

まとめ

  1. ポインタのnil値は「参照ができない」だけ
  2. インタフェースのnil値とポインタのnil値は違う

Goのnilはヤヤコシイ:frowning2:


  1. 「nilインタフェース値(nil interface value)」という語は規格書の中で1度だけ登場します。 

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4