※ Retty Advent Calendar 2018 11日目の記事です。
昨日は @shogo-tsutsumi さんの記事で、 広告KPIダッシュボード開発でPDCA回してみた でした。
はじめに
RettyではPHP・Kotlinなど様々な言語を使って開発を行なっています。
その中、今年Goを使ったマイクロサービスの開発も始まりました。
今回、個人的にGoの機能の中でも一番有能なinterfaceについて書きます。
GoのDuck Typingについて
Python, Rubyなどのdynamic言語でよく知られているDuck Typingですが、
Javaみたいにimplements
キーワードを使ってinterfaceの実装を宣言しなくても、
runtime時に、methodが実装されているかどうかをチェックしてくれる便利な仕組みです。
GoではPython, Rubyのような単純なDuck Typingよりもっと便利な実装になっています。
まず、runtime時にdynamicチェックが行われます。
... // import略
type I interface {
PrintName()
}
type T struct{}
func (*T) PrintName() {
fmt.Println("T")
}
func p(i interface{}) {
i.(I).PrintName()
}
func main() {
var t T
p(&t)
p(t) // panic: interface conversion: main.T is not main.I: missing method PrintName
}
playground
そして、staticでチェック可能なものはcompile時staticチェックもしてくれます。
... // import&declare略
func p(i I) { // interface{} -> I に変わったことで、compile時にチェックが可能になる
i.(I).PrintName()
}
func main() {
var t T
p(&t)
p(t) // cannot use t (type T) as type I in argument to p
}
本題に
compile時とruntime時でどちらもtype checkが行われる、
その便利なGo interfaceですが、割と理解しにくいところもあります。
まず、サンプルコードから
... // import略
type I interface {
PrintName()
}
type T1 struct{}
func (t *T1) PrintName() { // pointer receiver
fmt.Println("T1")
}
type T2 struct{}
func (t T2) PrintName() { // value receiver
fmt.Println("T2")
}
func main() {
var i I
var pT1 *T1
i = pT1
i.PrintName()
var t2 T2
i = t2
i.PrintName()
}
playground
上のコードは問題なく動きます。
そして、次のコード
... // import&declare略
func main() {
var i I
//var t1 T1
//i = t1 // cannot use t1 (type T1) as type I in assignment
//i.PrintName()
var pT2 *T2
i = pT2
i.PrintName() // panic: value method main.T2.PrintName called using nil *T2 pointer
}
まず、t1
はI
を実装していないため、i = t1
compileエラー、とても分かりやすいです。
でも、pT2
はI
を実装してないはずだけど、なんでi = pT2
compile通る?とても分かりません。
さらに、compile通ったけど、なんでpT2
のi.PrintName()
がruntimeエラーになる???もっと分かりません。
なんでi = pT2
ではcompileが通る?
色々調べた結果、gccgoのソースコードの中から理由を見つけました。
libgo/runtime/interface.h#15
libgo/runtime/go-convert-interface.c#24
gccgoのinterfaceの定義とinterface(gccgoの中ではstructもinterface扱い)間のコンバートを読んで理解しました。
- interface(
__go_interface
)がtype descriptor(__go_type_descriptor
)で表現される - interfaceのtype descriptorが3つの要素を持つ
- typeのpointer ->
I
のpointer - function pointerのリスト ->
PrintName
のfuntion pointerが入ってるリスト - valueのpointer -> 重要: ここは
pT2
のpointerなはずなんだけど、gccgoはheap領域をケチるため、どうやらpointerのpointerは保存しない。なので、ここinterfaceのvalueがpointerである場合、直接valueそのものを保存する (謎が解けた!!)
- typeのpointer ->
なので、pT2
をi
に代入する際に、i
のtype descriptorが持つのはpT2
そのものであって、pT2
のpointerではありません。
だから、i = pT2
はi = *pT2
(compileは通るが、nilをdereferenceしようとするので、ここはruntimeエラーになる)と同じ意味をします。
なんでpT2
のi.PrintName()
がnil pointerエラー?
type T struct {
a int
}
func (tv T) Mv(a int) int { return 0 } // value receiver
func (tp *T) Mp(f float32) float32 { return 1 } // pointer receiver
var t T
var pt *T
As with selectors, a reference to a non-interface method with a value receiver using a pointer will automatically dereference that pointer: pt.Mv is equivalent to (*pt).Mv.
As with method calls, a reference to a non-interface method with a pointer receiver using an addressable value will automatically take the address of that value: t.Mp is equivalent to (&t).Mp.
Goのspecに載せてる上の英語を読んでみればすでに分かったと思いますが、一応説明します。
- methodのreceiverがvalueの場合、pointerが自動的にdereferenceされる
-
pt.Mv
は自動的に(*pt).Mv
にされる
-
- methodのreceiverがpointerの場合、valueのアドレスが自動的に取得される
-
t.Mp
は自動的に(&t).Mp
にされる
-
なので、i.PrintName()
を呼ぶと、interfacei
が持っているvaluepT2
がreceiverとしてfunctionPrintName
を呼び出します。
つまり、pT2.PrintName()
、上のルールで自動的に(*pT2).PrintName()
にされます。
pT2
がnilな値なので、deferenceしようとすると、runtimeエラーが発生します。
(謎が解けた!!)
おわりに
Go interfaceはこんなちゃんと理解しようとすると、わりと理解しにくいところもあるけれど、
普段開発で使う分では全く困らないので、これからもGoをたくさん使って、もっと発信していこうと思います。
最後に、gccgoのソースコードが想定よりもとっても簡単で分かりやすかったので読んでて楽しかったです。
もし興味あれば、ぜひ読んでみてください。