Goのinterface実装可否はレシーバが値型かポインタ型かで異なるぞ
Table of Contents
Goで interface を実装した型のインスタンスに対して、 型アサーションができる場合とできない場合があることを知らなくてハマったというメモ。
TL;DR #
interface をポインタレシーバで実装した型の値リテラルに対して型アサーションを試みると失敗する。
もう少し詳しく #
Go での interface を実装する際、レシーバの型(値レシーバかポインタレシーバか)によって型アサーションの挙動が変わる。 値レシーバで実装した場合と、ポインタレシーバで実装した場合で違いがある。 完全なサンプルコードは https://go.dev/play/p/8XN92Xumhfp にある。
以下のような interface Animal
があるとする。
type Animal interface {
Walk()
}
この interface Animal
を実装した type Dog
を定義した。
Dog
は値レシーバに対して関数 Walk()
を実装している。
この場合、その値型(Dog{}
)もポインタ型(&Dog{}
)も interface Animal
を実装していることになる。
type Dog struct{}
func (d Dog) Walk() { fmt.Println("dog walking") }
// 値型の Dog は interface Animal を実装している
var _ Animal = Dog{}
// ポインタ型の Dog も interface Animal を実装している
var _ Animal = &Dog{}
同じく、 interface Animal
を実装した type Cat
を定義した。
ただし、 Dog
とは異なり ポインタレシーバ に対して関数 Walk()
を実装している。
この場合、ポインタ型(&Cat{}
)は interface として認識されるが、値型(Cat{}
)は 認識されない 。
type Cat struct{}
func (c *Cat) Walk() { fmt.Println("cat walking") }
// 値型の Cat は interface Animal を実装していることにならない!
// 以下の行はコンパイルエラーとなる
// > cannot use Cat{} (value of struct type Cat) as Animal value
// > in variable declaration: Cat does not implement Animal
// > (method Walk has pointer receiver)
// var _ Animal = Cat{}
// ポインタ型の Cat であれば interface Animal を実装している
var _ Animal = &Cat{}
これは、値リテラルから直接ポインタレシーバのメソッドを呼び出すことができないため。 以下のような呼び出しはエラーとなる。
// > cannot call pointer method Walk on Cat
Cat{}.Walk()
一度変数に入れればOK。
cvv := Cat{}
cvv.Walk()
実際にハマったこと #
今回の発端のはなし。
database/sql.DB.Exec()
でレコードのカラム値として使用していたstruct型のメソッドをポインタレシーバに統一したところ、 database/sql/driver.Valuer
での型アサーションに失敗した。
具体的には以下の部分で Valuer
として型アサーションされず、
https://github.com/golang/go/blob/6953ef86cd72a835d398319c4da560c8b78ba28e/src/database/sql/driver/types.go#L247
その後のどのパターンにも該当しない場合のエラーとして処理されてしまう。 https://github.com/golang/go/blob/6953ef86cd72a835d398319c4da560c8b78ba28e/src/database/sql/driver/types.go#L294
なぜ型アサーションに失敗するのかがわからずハマったというわけ。
再現用のサンプルコードはこちら。
なぜハマったのか #
2番目のサンプルコードで以下のようなコードで interface の実装をチェックしていた。
ポインタ型の変数に対してならポインタレシーバのメソッドを呼び出すことができるので、
ItemSchedule
は driver.Valuer
を実装していることになる。
つまり、この行は コンパイルエラーにはならない 。
var _ driver.Valuer = (*ItemSchedule)(nil)
しかし、このチェックはポインタ型の変数に対してしか有効ではない。 以下のように値型の変数に対して型アサーションをしてみれば、interface を実装できていないことにすぐに気づくことができる。
var _ driver.Valuer = ItemSchedule{}
この行はビルドすると以下のようなエラーになる。
cannot use ItemSchedule{} (value of struct type ItemSchedule) as driver.Valuer value in variable declaration: ItemSchedule does not implement driver.Valuer (method Value has pointer receiver)
「メソッド Value はポインタレシーバだから driver.Valuer を実装できていない」というそのままのことが書いてある。
おわり #
今回のハマりは手癖で書いていた nil をキャストして型アサーションに使う、 というイディオムが意味をなさない場合を把握できていなかったのが原因と言える。
Go はそこそこの時間をかけて書いてきたつもりだが、たまにこういう基本的な仕様が把握できておらずハマることがある。 面倒くさがらずに都度調べて納得していかなければ。