Goクイズ Advent Calendar 2020/12/16(wed)

5 min read読了の目安(約4800字

この記事は、Goクイズ Advent Calendar 2020の16日目の記事です。

問題

https://play.golang.org/p/v7M1kD3qEGJ
package main

import "fmt"

type T struct {
	x int
}

func (t T) Get() int {
	return t.x
}
func (t T) Set(x int) T {
	t.x = x
	return t
}
func (t *T) Add(x int) *T {
	t.x += x
	return t
}
func main() {
	var t T
	fmt.Println(t.Set(1).Add(1).Get())
}

選択肢

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

考えてみましょう!

  _____        _                                               
 | ____|_ __  (_) ___  _   _                                   
 |  _| | '_ \ | |/ _ \| | | |                                  
 | |___| | | || | (_) | |_| |                                  
 |_____|_| |_|/ |\___/ \__, |                                  
            |__/    _  |___/                                   
  ___  ___ | |_   _(_)_ __   __ _                              
 / __|/ _ \| \ \ / / | '_ \ / _` |                             
 \__ \ (_) | |\ V /| | | | | (_| |                             
 |___/\___/|_| \_/ |_|_| |_|\__, |                             
   ____                     |___/     _                        
  / ___| ___         __ _ _   _(_)___| |                       
 | |  _ / _ \ _____ / _` | | | | |_  / |                       
 | |_| | (_) |_____| (_| | |_| | |/ /|_|                       
  \____|\___/       \__, |\__,_|_/___(_)                       
   __ _ _ __  _____    |_|____ _ __  (_)___                    
  / _` | '_ \/ __\ \ /\ / / _ \ '__| | / __|                   
 | (_| | | | \__ \\ V  V /  __/ |    | \__ \                   
  \__,_|_| |_|___/ \_/\_/ \___|_|    |_|___/                   
   __ _| |_  | |__   ___ | |_| |_ ___  _ __ ___                
  / _` | __| | '_ \ / _ \| __| __/ _ \| '_ ` _ \               
 | (_| | |_  | |_) | (_) | |_| || (_) | | | | | |              
  \__,_|\__| |_.__/ \___/ \__|\__\___/|_| |_| |_|     _      _ 
   ___  / _| | |_| |__ (_)___    __ _ _ __| |_(_) ___| | ___| |
  / _ \| |_  | __| '_ \| / __|  / _` | '__| __| |/ __| |/ _ \ |
 | (_) |  _| | |_| | | | \__ \ | (_| | |  | |_| | (__| |  __/_|
  \___/|_|    \__|_| |_|_|___/  \__,_|_|   \__|_|\___|_|\___(_)

解説

A method call x.m() is valid if the method set of (the type of) x contains m and the argument list can be assigned to the parameter list of m. If x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m():

  • メソッド呼び出しx.m()は、xの型のメソッドセットがmを含んでおり、かつ与えた引数リストがmのパラメータリストに代入可能であるとき、有効です。
  • もしxがアドレス化可能で、かつ&xのメソッドセットがmを含むならば、x.m()&x.m()の省略表現となります。

今回の問題を再掲します。

type T struct {
	x int
}

func (t T) Get() int {
	return t.x
}
func (t T) Set(x int) T {
	t.x = x
	return t
}
func (t *T) Add(x int) *T {
	t.x += x
	return t
}
func main() {
	var t T
	fmt.Println(t.Set(1).Add(1).Get())
}

この中には以下の3つのメソッド呼び出しがあります。

  • t.Set(1)
  • t.Set(1).Add(1)
  • t.Set(1).Add(1).Get()

1つめのt.Set(1)はtの型がTであってTのメソッドセットにSetが含まれるため問題ありません。引数の代入可能性ももちろん満たしています。

2つめのt.Set(1).Add(1)をみると、t.Set(1)T型なのでポインターレシーバーメソッドのAddをメソッドセットに含みません。それではt.Set(1)をアドレス化した&t.Set(1)の省略表現とみなせるでしょうか?そのためにはt.Set(1)がアドレス化可能でなければなりません。そこでアドレス化可能性についてみてみましょう。

Addressability より:

For an operand x of type T, the address operation &x generates a pointer of type *T to x. The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array. As an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal.

これによると、addressableな場合は次の5つです。

  • 変数である
  • pointer indirection *pである
  • スライスに対するインデックス演算の結果である
  • アドレス化可能なstructに対するフィールドセレクターである
  • アドレス化可能な配列に対するインデックス演算の結果である

t.Set(1)という式は上記のいずれにも当てはまりませんから、アドレス化可能ではなく、t.Set(1).Add(1)というメソッド呼び出しはillegalです。したがって、本問のコードはコンパイルできず、次のようなエラーメッセージを出力します。

./prog.go:22:22: cannot call pointer method on t.Set(1)
./prog.go:22:22: cannot take the address of t.Set(1)

解説の流れと同じことをエラーメッセージに書いてくれています。

なお、Addrssabilityについて仕様書から引用した段落の最後の1文は、composite literalがaddressbleではないことを前提としています。この"As an exception"とは、「address operator&が適用可能なのはaddressableなoperandのみであるという原則」に対する例外を意味しており、「composite literalはaddressableではないけれども&が適用可能である」という意味です。

これは本問のメソッドを使って次のようなコードを書くと分かります。playgroundのリンクをつけたので確認してみてください。

T{}.Add(1) // compile error. T{}がaddressableではないから。

解説サマリー

  • Goのメソッド宣言には、value receiverとpointer receiverという2つの宣言の仕方がある。
  • 記述の簡便性のため、value.PointerReceiverMethod()(&value).PointerReceiverMethod()と解釈される。
  • ただし、この記法が有効であるためには、valueがアドレス化可能(addressable)でなければならない。アドレス化可能な場合は5つある。
  • なお、composite literalsは、&をつけることができるがアドレス化可能ではない。

正解

(1)の、compile(build) errorが正解です。