🙌

Goquiz 1

2020/11/26に公開

twitterで出したクイズのうちの1つが好評だったのと、自分としても面白いところだったので解説を書いておくことにしました。

問題

次のGo言語のコードを実行するとどうなるでしょうか?(正解は記事の一番下に書きます)

package main

import (
	"fmt"
)

type X struct {
	v *int
}

func main() {
	var p *X = &X{}
	fmt.Println(*p.v)
}

選択肢

  1. nilと表示される
  2. build(compile) error
  3. panic

簡潔な解説

var p *X = &X{}と書いたとき、composite literal X{}で指定されていないフィールドvはゼロ値で初期化されますので、*int型のゼロ値であるnilが入っています。つまり、この行は次のコードと等価です。

var p *X = &X{
    v: nil,
}

ここまでを前提として、次の行が問題です。*p.vという式をみると、次のどちらを意味するのか曖昧に思えるのではないでしょうか。

  1. pをdereferenceしてから、vというフィールドにアクセスする。つまり、(*p).vと同じ意味。
  2. p.vを評価してから、そのあとdereferenceする。つまり*(p.v)と同じ意味。

1.と解釈すると答えはnilになりますが、正しい解釈は2.です。

ポインタ型*Xの変数を表す識別子pに対してp.vという式は(型Xに定義された)フィールドvを表します(※)。その値は*int型のゼロ値であるnilでしたから、これをdereferenceするとnil pointer dereferenceとなりpanicが起こります。

Go言語仕様書に興味のある人向けの解説

さて、*p.vが以上のように解釈されることは私も以前に見たこと(ハマったこと)がありましたが、その理由は知りませんでした。なんとなくプログラミングでよくある「演算子の優先順位」がここでも定義されていて、*よりも.のほうが優先されるのかな、というように理解していました。同じような方は多いのではないでしょうか?

ところがこれは違うようだ、ということが最近わかりました。正しい説明をすると、そもそも*pという式は、x.yという式(セレクタ式、selector expressionといいます)の左側のパーツxなる資格がないのです。言い換えると、上記の解釈1と解釈2に「曖昧性があるので別なルールを設けてどちらかに決めている」のではありません。そうではなく、解釈1はGo言語の仕様として認められないがゆえに、もともと曖昧さはなかったのです!

これを理解するためには、いくつかのEBNFによる定義式を仕様書から引用する必要があります。

PrimaryExpr =
	Operand |
	Conversion |
	MethodExpr |
	PrimaryExpr Selector |
	PrimaryExpr Index |
	PrimaryExpr Slice |
	PrimaryExpr TypeAssertion |
	PrimaryExpr Arguments .

Selector       = "." identifier .

ここにはprimary expressionのありうる形が列挙されていて、そのうちの1つがselector expressionと呼ばれる形です。わかりやすく書き下すと次のようになります。

SelectorExpr = PrimaryExpr "." identifier .

たとえばxがprimary expressionであれば、x.vのような式はselector expressionと認められることになります。このselector expressionというのが、普段フィールドへのアクセスやメソッドへのアクセスに用いている式ということです。

ということは、*p.vという式を(*p).vと同じ意味に解釈できるためには、*pという式がprimary expressionでなければなりません。(※なお話が前後しますが、式(*p)はprimary Expressionです。)ところが、仕様書を読んでいくと*pはprimary expressionではないことがわかるのです。それゆえに、*p.vはselector expressionではないということが言えて、先ほどの「解釈1」が成り立たないことがわかります。

それでは*pの正体を調べてみましょう。まず、pは識別子(identifier)だということは既知としましょう。つぎに仕様書の記述を見てみます。

Operand     = Literal | OperandName | "(" Expression ")" .
OperandName = identifier | QualifiedIdent .

identifierはOperandNameであり、OperandNameはOperandなので、pはOperandだとわかりました。さきほど

PrimaryExpr =
	Operand |
	Conversion |
	MethodExpr |
	PrimaryExpr Selector |
	PrimaryExpr Index |
	PrimaryExpr Slice |
	PrimaryExpr TypeAssertion |
	PrimaryExpr Arguments .

Selector       = "." identifier .

とありましたから、OperandであるpはPrimaryExpr(primary expression)でもあることがわかりました。

それでは*pとは何なのでしょう?

Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr  = PrimaryExpr | unary_op UnaryExpr .

unary_op   = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .

*はunary_op(unary operator, 単項演算子)です。unary operator*とprimary expressionpを結合した式である*pはprimary expressionになるのでしょうか?もう一度primary expressionの定義を見ます。

PrimaryExpr =
	Operand |
	Conversion |
	MethodExpr |
	PrimaryExpr Selector |
	PrimaryExpr Index |
	PrimaryExpr Slice |
	PrimaryExpr TypeAssertion |
	PrimaryExpr Arguments .

Selector       = "." identifier .

primary expressionの可能な形はこれで尽くされています。式*pはどれにも該当しません(たとえばOperandにも該当しません)から、primary expressionではないことがわかりました。よって*p.vはselector expression(セレクタ式)ではありません。

更に興味のある方へ

Go言語の仕様書をじっくり和気あいあいと読んでいく読書会があります!

https://gospecreading.connpass.com/event/197221/

途中からの参加も歓迎とのことです!いかがでしょうか?

(※) これについては仕様書に以下の記述があります。

For a value x of type T or *T where T is not a pointer or interface type, x.f denotes the field or method at the shallowest depth in T where there is such an f. If there is not exactly one f with shallowest depth, the selector expression is illegal.

正解

(3) panic

Discussion