こんにちは、いっちゃんです。10ANTZでは、サーバーサイドのソフトウェア開発に携わっています。
本記事では、2023年9月4日に開催された社内勉強会の復習をします。10ANTZのエンジニアの間では不定期で勉強会が開催されており、業務と直接関係あるかどうかに関わらず幅広いテーマが取り上げられています。今回は、Goクイズと題しまして、プログラミング言語Goの振る舞いに関するクイズを出題しました。以下では、ここで出題したクイズの中で、nilの振る舞いに関するものについてピックアップして解説します。
目次
クイズの形式
クイズは、短いGoのプログラム(main.go)と4つの選択肢からなります。選択肢は以下の4つです。
- コンパイルエラー(go build -o main main.goが失敗)
- 実行時エラー(./main を実行した時,終了ステータスが0以外)
- Xを出力(./main を実行した時,標準出力にXが含まれる)
- Yを出力(./main を実行した時,標準出力にYが含まれる)
解答では、これらの選択肢の中から該当するものを全て選択します。
出題したクイズ
以下に勉強会で出題したクイズとその解答を示します。
クイズ1
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package main import "fmt" func f(a any) { if a == nil { fmt.Println("X") } else { fmt.Println("Y") } } func main() { var a *int = nil f(a) } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
4
nil と型付きの nil は別物です。f において a は nil ではなく (*int)(nil) となるため、a == nil は false となります。
https://go.dev/ref/spec#Comparison_operators
クイズ2
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package main import "fmt" func main() { var a any switch a.(type) { default: panic("fail") case nil: fmt.Println("X") case any: fmt.Println("Y") } } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
3
nil と型付きの nil は別物です。a が nil の時 a は型を持ちません。この時、a.(type) は nil にマッチします。
https://go.dev/ref/spec#Type_switches
クイズ3
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 |
package main import "fmt" func main() { var a any if _, ok := a.(any); ok { fmt.Println("X") } else { fmt.Println("Y") } } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
4
nil と型付きの nil は別物です。a が nil の時 a は型を持ちません。この時、型 T に対して型アサーション a.(T) は失敗します。
https://go.dev/ref/spec#Type_assertions
解説
これらのクイズは全て、Goにおける nil に関して私が分かりにくいと感じた振る舞いをもとに作成しました。これらの振る舞いについて分かりにくいと感じる人は他にもいると思います。実際、上のクイズについて、社内勉強会における正答率(回答者のうち、正答した人の割合)は、クイズ1から順に0.89、0.11、0.00となり、正答率の極端に低いクイズが存在します。
これらの振る舞いを理解するための私の考え方を以下に示します。
下の図(Goにおける値の分類とnil)に示すように、Goにおいて「変数に代入できるもの」は、「値として無効なことを表すための nil 」と「値として有効なもの」の2つに分類できます。さらに、「値として有効なもの」は「型付きのnil値」と「型付きのnil以外の値」に分類することができます。そのように分類すると、「値として無効なことを表すための nil」と「型付きの nil 値」は、どちらも nil という記述により表現できるにも関わらず、別物であることが分かります。実際、Goの標準パッケージの一つであるreflectパッケージのValueはこの分類に沿った設計となっているとみなすことができます。
nilという表記には、「値として無効なことを表すための nil」と「型付きの nil 値」の2通りの意味があることを示しましたが、コンパイラはこれらの解釈を文脈に応じて切り替えます。Goにおけるnilの振る舞いを理解するのが難しい理由の一つは、このような同じ表記であるにも関わらず意味が切り替わることだと思います。この振る舞いは、具体的には、以下のように整理して理解することができます。
- nilと記述されたのが、変数の初期化または代入の場合
- 代入先の変数がインターフェース型で宣言されているときは、「値として無効なことを表すnil」と解釈される。
-
1var err error = nil // 変数がerrorインターフェース型で宣言されているため、「値として無効なことを表すnil」と解釈される
-
- それ以外のときは、「型付きnil値」と解釈される。
-
1var ptr *int = nil // 変数が*int型で宣言されているため、「型付きnil値」(*int)(nil)と解釈される
-
- 代入先の変数がインターフェース型で宣言されているときは、「値として無効なことを表すnil」と解釈される。
- nilと記述されたのが、比較演算の中である場合
- 片方のオペランドの静的な型がインターフェース型のときは、「値として無効なことを表すnil」と解釈される。
-
1err != nil // errの静的な型がerrorインターフェースであるため、「値として無効なことを表すnil」と解釈される
-
- それ以外のときは、「型付きnil値」と解釈される。
-
1ptr != nil // ptrの静的な型が*intであるため、「型付きnil値」(*int)(nil)と解釈される
-
- 片方のオペランドの静的な型がインターフェース型のときは、「値として無効なことを表すnil」と解釈される。
まとめ
本記事では、社内勉強会で出題したGoの振る舞いに関するクイズのうち、nilに関するものをピックアップして紹介し、解説しました。解説では、Goにおける値の分類からnilには、「値として無効なことを表すための nil」と「型付きの nil 値」の2通りの意味があることを説明し、これらがコンパイラによってどのように解釈されるかを型に着目して示しました。