こんにちは、いっちゃんです。10ANTZでは、サーバーサイドのソフトウェア開発に携わっています。
本記事では、Goクイズと題しまして、プログラミング言語Goの振る舞いに関するクイズを出題します。今回は、特に、アドレスの振る舞いに関するものについてピックアップして解説します。本記事は、すでに公開されている記事「Goクイズ(nil編)https://developers.10antz.co.jp/archives/3835 」、「Goクイズ(defer編)https://developers.10antz.co.jp/archives/3837 」、「Goクイズ(string編)https://developers.10antz.co.jp/archives/3839」の続きとなっています。
目次
クイズの形式
前回と同様に、クイズは、短いGoのプログラム(main.go)と4つの選択肢からなります。選択肢は以下の4つです。
- コンパイルエラー(go build -o main main.goが失敗)
- 実行時エラー(./main を実行した時,終了ステータスが0以外)
- Xを出力(./main を実行した時,標準出力にXが含まれる)
- Yを出力(./main を実行した時,標準出力にYが含まれる)
回答では、これらの選択肢の中から該当するものを全て選択します。
クイズの出題
以下にクイズを出題し、その解答を示します。番号は前回の続きからとなっています。
クイズ10
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package main import "fmt" type T struct{} func (*T) f() { fmt.Println("X") } func main() { m := map[int]T{} m[0].f() } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
1
mapに対する添え字アクセス m[0] はaddressableではありません。m[0].f を (&m[0]).f の省略形だと見なすことができないため「cannot call pointer method f on T」というコンパイルエラーとなります。
※解答は、ネタバレ防止のため背景色と同じ色で記載されています。
クイズ11
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package main import "fmt" type T struct{} func (*T) f() { fmt.Println("X") } func main() { var t *T t.f() } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
3
t == (*T)(nil) となるため、nil参照が発生するように思うかもしれません。しかし実際には,メソッド内で t が参照されないため,nil参照は発生しません。
https://go.dev/ref/spec#For_range
※解答は、ネタバレ防止のため背景色と同じ色で記載されています。
クイズ12
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package main import "fmt" type T struct{} func (T) f() { fmt.Println("X") } func main() { var t *T t.f() } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
2
t == (*T)(nil) となっており,t.f は (*t).fの省略形と見なされます。そのため、デリファレンスされる時に nil 参照が発生し、「panic: runtime error: invalid memory address or nil pointer dereference」という実行時エラーとなります。
※解答は、ネタバレ防止のため背景色と同じ色で記載されています。
クイズ13
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package main import "fmt" type T struct{} func (*T) f() { fmt.Println("X") } func main() { var t T t.f() } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
3
変数 t はaddressableであるため t.f は (&t).f の省略形だと見なされます。
※解答は、ネタバレ防止のため背景色と同じ色で記載されています。
クイズ14
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package main import "fmt" type T struct{} func (*T) f() { fmt.Println("X") } func main() { (T{}).f() } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
1
コンポジットリテラル T{} は &演算子をつけることができるにもかかわらずaddressableではありません。そのため(T{}).f を (&T{}).f の省略形だと見なすことができず「cannot call pointer method f on T」というコンパイルエラーとなります。
※解答は、ネタバレ防止のため背景色と同じ色で記載されています。
解説
これらのクイズは全て、Goにおけるメソッドがポインタレシーバーに対して定義されているのか、値レシーバーに対して定義されているのかに関して私が分かりにくいと感じた振る舞いをもとに作成しました。これらの振る舞いについて分かりにくいと感じる人は他にもいると思います。
これらの振る舞いを理解するためには、Goにおけるメソッド呼び出しの省略記法とアドレス演算子について理解する必要があると思います。以下ではそのことについて解説します。
メソッド呼び出しの省略記法
ポインタレシーバー.値レシーバーメソッド という記述は、 (*ポインタレシーバー).値レシーバーメソッド の省略形と見なすことができます。ここでデリファレンス(ポインタの指す先の値に対する参照)が発生するため、ポインタレシーバーがnilポインタだった場合はnil参照によるpanicとなります。
また、 値レシーバー.ポインタレシーバーメソッド という記述は、 (&値レシーバー).ポインタレシーバーメソッド の省略形と見なすことができます。ただし、そのように見なすためには値レシーバーがaddressableである必要があり、そうでない場合にはコンパイルエラーとなります。
https://go.dev/ref/spec#Method_values
アドレス演算子
アドレス演算子「&」はaddressableな値のポインタを取得する演算子です。addressableな値とは、以下のいずれかに該当する値です。
- 変数
- ポインタのデリファレンス
- スライスに対する添え字アクセス
- アドレス可能な構造体に対するフィールドセレクタ
- アドレス可能な配列に対する添え字アクセス
また、例外的にコンポジットリテラル(T{},[]int{},map[string]int{},[5]string{}等)に対してもアドレス演算子を適用することができます。
例えば、クイズ10のmap mに対する添字アクセスm[0]はaddressableではありません。他にも関数 f の呼び出し f() やstring sに対する添字アクセス s[0]もaddressableではありません。また、クイズ14のコンポジットリテラル T{} は、アドレス演算子を適用可能ではありますが、addressableではないことに注意が必要となります。
https://go.dev/ref/spec#Address_operators
まとめ
本記事では、Goの振る舞いに関するクイズとして、メソッドに関するものをピックアップして出題しました。また、解説では、メソッド呼び出しの省略記法、addressableな値とアドレス演算子について説明しました。特に「コンポジットリテラルはアドレス演算子を適用可能ではあるが、addressableではない」という点には注意が必要です。