はじめに
本記事は, Goクイズ Advent Calender 2020の12/24用に書かれました。
昨日は, @binzumeさんの「Goクイズ: int = float64」でした。
Index expressionsに関する問題を出そうと思ったのですが, 以前Twitterで出題した問題と似ている気がしたので, 急遽Type assertionsに関する問題に取り替えました。
では, 本題に入ります。
問題
以下のコードを実行すると, どのように出力されるでしょうか?
Playground
package main
import (
"fmt"
)
type I interface {
Hello()
}
type T struct{}
func (T) Hello() {
fmt.Println("Hello!")
}
func main() {
var i I = nil
v, _ := i.(T)
v.Hello()
}
- compile error
- i.(T)の代入部分でrun-time panic
- v.Hello()の実行部分でrun-time panic
- Hello!
解答と解説
解答と解説
TL;DR
- 特別な形式の代入か初期化に用いられる型Tへのアサーションは, untyped bool値を追加で生成する
- 型アサーションが保持されていない場合, 1つ目の返り値は型Tのゼロ値, 2つ目の返り値はuntyped boolのfalseとなる
- この場合, run-time panicは発生しない
解説
まず, 型アサーション(Type assersions)について確認します。
For an expression
x
of interface type and a typeT
, the primary expressionx.(T)
asserts that x is not nil and that the value stored inx
is of typeT
. The notation x.(T) is called a type assertion.
インタフェース型の式x
と型T
について下記の通り書かれています。
-
x.(T)
という表記法を型アサーションと呼ぶ -
x.(T)
はx
がnilではなくx
に格納されている値が型T
のものであることを保証する
ということは, x.(T)
の型アサーションが実行される場合, x
がnilでないことが保証される訳です。しかし, 本問ではi.(T)
におけるiはnilです。これはどういうことなのでしょうか?その答えは後述されている文章にあります。
A type assertion used in an assignment or initialization of the special form
v, ok = x.(T)
v, ok := x.(T)
var v, ok = x.(T)
var v, ok T1 = x.(T)
yields an additional untyped boolean value. The value of ok is true if the assertion holds. Otherwise it is false and the value of v is the zero value for type T. No run-time panic occurs in this case.
- これらの, 特別な形式の代入か初期化に用いられる型アサーションは型なしのブール値を追加で生成する
-
ok
の値はアサーションが保持されていればtrue
である - それ以外の場合は
false
であり, vの値は型T
のゼロ値である - この場合, run-time panicは発生しない
すなわち, この形式で代入および初期化をした場合, run-time panicは発生せず, 1つ目の返り値に型T
の変数が格納されます。
ここで, "この形式"というのは, 2つ目の返り値まで受けとる形式を指すと私は考えています。なぜなら, 2つ目の形式を受け取らない場合, 下記のコードになり, run-time panicとなるためです。
package main
import (
"fmt"
)
type I interface {
Hello()
}
type T struct{}
func (T) Hello() {
fmt.Println("Hello!")
}
func main() {
var i I = nil
var v = i.(T) // panic: interface conversion: main.I is nil, not main.T
v.Hello()
}
これを見た時に, 型アサーション(Type assertions)の冒頭に書かれていた, 「x.(T)
はx
がnilではないことを保証する」という定義と矛盾するように感じました。
2020/12/24追記:
良く考えたら, 2つ目の返り値にfalseが格納されており, 型アサーションは保持されていません。そのため, 冒頭に書かれていた定義は適用されないので何の問題もありませんでした。
何はともあれ, この場合型T
という情報は保持されるので, 当然v.Hello()
も実行できます。
結果として, Hello!
が出力されます。
おわりに
対策として, 2つ目の返り値を受け取った場合は, 下記の通り分岐を入れると良いと思います。
package main
import (
"fmt"
"log"
"os"
)
type I interface {
Hello()
}
type T struct{}
func (T) Hello() {
fmt.Println("Hello!")
}
func main() {
var i I = nil
v, ok := i.(T)
if !ok {
log.Println("invalid assertion")
os.Exit(1)
}
v.Hello()
}
2021/01/14追記:
この形式は, nil
から""
(empty string)に変換する際にも使えますね。Nullを含むレコードがあるときに使えそう。
package main
import (
"fmt"
)
func main() {
var n interface{} = nil
if _, ok := n.(string); !ok {
n = ""
}
fmt.Println("n: ", n.(string))
fmt.Println("n==nil: ", n == nil)
}
ここまでお読みいただきありがとうございました。
明日は, @tenntennさんのラスボス問題があるようです。楽しみですね~。