Help us understand the problem. What is going on with this article?

Go interfaceの理解しにくいところ

More than 1 year has passed since last update.

※ 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チェックが行われます。

dynamic-check.go
... // 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チェックもしてくれます。

static-check.go
... // 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
}

playground

本題に

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
}

playground

まず、t1Iを実装していないため、i = t1compileエラー、とても分かりやすいです。
でも、pT2Iを実装してないはずだけど、なんでi = pT2compile通る?とても分かりません。
さらに、compile通ったけど、なんでpT2i.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そのものを保存する (謎が解けた!!)

なので、pT2iに代入する際に、iのtype descriptorが持つのはpT2そのものであって、pT2のpointerではありません。
だから、i = pT2i = *pT2(compileは通るが、nilをdereferenceしようとするので、ここはruntimeエラーになる)と同じ意味をします。

なんでpT2i.PrintName()がnil pointerエラー?

spec
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のソースコードが想定よりもとっても簡単で分かりやすかったので読んでて楽しかったです。
もし興味あれば、ぜひ読んでみてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした