どうも ryo_grid です。
最近は自作RDBMSしています。皆さんもどうですか?
自作RDBMSやろうぜ!
というのはさておき、自身はGoのinterfaceの挙動がしばらくよくわからなくてハマったのですが、そこらへんの簡潔な解説がGo公式サイトにも無いし、適当な技術記事も見当たらないなーと思ったので自分で書くことにしました。
細かいことを気にしなければ話は単純です。
なお、ハマりやすいところについてだけ解説するので、それ以前の知識は別途得てください。
Goのinterfaceな型はポインタを保持する箱
こちらの記事により詳しく書いてありますが、Goのinterfaceとして定義された型で型付けされた変数など(以降、interfaceな型)は、型情報とインタフェースを実装した構造体へのポインタを格納する構造体として内部的には実装されています。
記事から抜粋すると、内部構造は以下で、tabというメンバが型情報、dataというメンバは上で述べたポインタです。
type iface struct {
tab *itab
data unsafe.Pointer
}
つまるところ、interfaceな型は、ポインタ型のようなものだと考えることができます。
ただ、ややこしいのが、あるinterfaceな型Aの変数(等)があった時に、それに対して、型Aを実装した構造体(型)の値とポインタのいずれもがつっこめる(≒代入・実質的に見た時のアップキャスト)点です。
コード例で確認
例えば、以下のようなコードを書いて動かしてみると、値もポインタもHoge型に代入ができて、どちらでも実装しているメソッドを呼び出せることが確認できます。
package main
import (
"fmt"
)
type Hoge interface {
HogeFunc()
}
type HogeImpl struct {
msg string
}
func (hoge HogeImpl) HogeFunc() {
fmt.Println(hoge.msg)
}
func main() {
hogeImpl := HogeImpl{"hoge!!!"}
var hogeZ Hoge = hogeImpl
var hogeP Hoge = &hogeImpl
hogeZ.HogeFunc()
hogeP.HogeFunc()
}
また、うまいこと設計できていればあまりやる機会はないと思われますが、型アサーションを行うことで、interfaceな型に一度代入していても、実装である構造体の値やポインタに戻すことができます。
package main
import (
"fmt"
)
type Hoge interface {
HogeFunc()
}
type HogeImpl struct {
msg string
}
func (hoge HogeImpl) HogeFunc() {
fmt.Println(hoge.msg)
}
func main() {
hogeImpl := HogeImpl{"hoge!!!"}
var hogeZ Hoge = hogeImpl
var hogeP Hoge = &hogeImpl
var hogeZ_ HogeImpl = hogeZ.(HogeImpl)
var hogeP_ *HogeImpl = hogeP.(*HogeImpl)
hogeZ_.HogeFunc()
hogeP_.HogeFunc()
}
ただし、interfaceの実装である型がメソッドをポインタレシーバを受ける形で実装している場合、値を代入して、当該メソッドを呼び出すとエラーになります。
package main
import (
"fmt"
)
type Hoge interface {
HogeFunc()
}
type HogeImpl struct {
msg string
}
func (hoge *HogeImpl) HogeFunc() {
fmt.Println(hoge.msg)
}
func main() {
hogeImpl := HogeImpl{"hoge!!!"}
// 代入しているのは値
var hogeZ Hoge = hogeImpl
hogeZ.HogeFunc()
}
しかし、値レシーバを受ける形で実装している場合に、ポインタを代入してもエラーになりません。
ややこしいですね。
package main
import (
"fmt"
)
type Hoge interface {
HogeFunc()
}
type HogeImpl struct {
msg string
}
func (hoge HogeImpl) HogeFunc() {
fmt.Println(hoge.msg)
}
func main() {
hogeImpl := HogeImpl{"hoge!!!"}
// 代入しているのはポインタ
var hogeP Hoge = &hogeImpl
hogeP.HogeFunc()
}
interfaceな型に入れておくものは値とポインタのどちらかに統一しておこう
- interfaceな型は上のような挙動をするわけですが、中に入っているものが(入れるべきものが)ポインタなのか値なのか分からなくなると混乱するので、コードベースやプロジェクトの単位で統一しておくのが無難です
- 2つのポイント
- interfaceのことを一旦忘れて考えてみると、メソッドが、定義されている構造体のメンバを更新しようと思った場合、通常、レシーバはポインタで受けて扱う
- 上のコード例で示したように、値レシーバを受ける形でメソッドが定義されていても、ポインタが格納された場合、当該メソッドは呼び出せる
- 上記のポイントを踏まえて考えると、特別な理由がなければ、ポインタを格納する形に統一するのが無難だと考えています
- 一方で、値を格納する形に統一すると?
- interfaceな型に値を代入したり、返り値の型がinterfaceな型な関数・メソッドで値を返した場合、interfaceな型ではない型に対する場合の挙動と同様、まず値がコピーされます。そして、その領域へのポインタを内部に持つ、という形になります
- この挙動から、参照(ポインタ)を共有したくない場合は値を入れてやれば良いという発想になりそうですが、その場合、上のコード例で示したように、ポインタレシーバを受ける形で実装されたメソッドがinterfaceな型において呼び出せなくなります
- 従って、上のような必要性がある場合も、ポインタを格納する規約にしておき、その中で参照を共有したくない場面があれば、一旦別の変数に代入するなどの方法でコピーしたデータを用意し、そのポインタを渡すよう対応するのがよいと考えています
ちなみに(ある意味ここが一番重要)
-
interfaceな型は特殊な型なので、*hogeで値を得ようとか、&hogeでポインタを触ろうとかしてもうまくいきませんし、うまくいっても混乱の元なので、やめておきましょう
- ※: ただし、下の "関連する情報源" のところにあげている記事で解説されているように、稀ですが、ライブラリのメソッドや関数からデータを受け取るためにポインタを渡すといった使い方をする場面はあるようです
- 同様の理由からinterfaceな型のポインタ型(上のコード例で言うと*Hoge)を使うのもやめておくのが吉です
関連する情報源
-
Goでinterfaceのポインタを使わない理由と使う場面 - Qiita
- この文章を一度投稿したあとに知った記事ですが、やはりinterfaceな型のポインタ型(ポインタ)は扱わない、というのがGoでの一般的なお作法のようです
- ただし例外もある、という話が上の記事では書かれています
- 記事で言及されているerrorsパッケージのIs,As関数については下の記事が分かりやすかったです
最後に
私の理解・経験が不十分で、不適切なサジェスチョンを行っていることもあるかもしれません。
その際は、Goつよつよエンジニアの方々にご指摘など頂ければ幸いです。
enjoy!