Goのinterfaceをデータ構造から理解する
この記事は 2021 Go Advent Calendar 3日目 の記事です。
はじめに
Goにはinterfaceと呼ばれるデータ構造が存在します。
interface typeは良くも悪くもGoのランタイムの中で柔軟な振る舞いを持ちます。
今回は改めてGoのinterfaceを学習することで、今日からのGopher lifeをより良くすることを目的とします。
note
この記事はGo1.17の時点での記事です。
Go1.18から導入予定のGenericが導入されることで、一部よりいい書き方が生まれる可能性があります。
interfaceの使い方
1つは他言語の文脈で用いられるような抽象化のためのinterfaceです。
以下がサンプルコードです。
package main
import "fmt"
type Go interface {
Hello()
}
var _ Go = (*gopher)(nil)
type gopher struct {
world string
}
func (g *gopher) Hello() {
fmt.Println(g.world)
}
func main() {
g := gopher{
world: "hello gopher",
}
g.Hello()
}
hello gopher
Program exited.
この機能によりGoではダックタイピングを行うことが可能です。
しかし、ダックタイピングを持つ動的型付けの言語と異なり、CT(コンパイルタイム)で静的型付けに則ったチェックを行うことができます。
そのため、byte
を引数とする場所で誤って int
を渡してしまうなどのミスを防ぐことが可能です。
また以下のようなコードがあるとします。
package main
import "fmt"
type Go interface {
Hello()
}
var _ Go = (*gopher)(nil)
type gopher struct {
world string
}
func (g *gopher) Hello() {
fmt.Println(g.world)
}
func hello(g Go) {
g.Hello()
}
func main() {
g := &gopher{
world: "hello gopher",
}
hello(g)
}
hello gopher
Program exited.
このように引数として取ることで、Gointerfaceが持つHelloメソッドの正しいsignatureを持つあらゆる型を受け入れるといったことも可能です。
そして、先ほど述べたランタイムの中で動的に振る舞いを変えることも可能です。
package main
import (
"fmt"
"strconv"
)
type Go interface {
String() string
}
var _ Go = (*gopher)(nil)
type gopher struct {
world string
}
func (g *gopher) String() string {
return g.world
}
func main() {
g := gopher{
world: "hello gopher",
}
fmt.Println(toString(&g))
n := 1
fmt.Println(toString(n))
var f float64 = 1
fmt.Println(toString(f))
fmt.Println(toString(g))
}
func toString(any interface{}) string {
if v, ok := any.(Go); ok {
fmt.Println("Go")
return v.String()
}
switch v := any.(type) {
case int:
fmt.Println("int")
return strconv.Itoa(v)
case float64:
fmt.Println("float")
return strconv.FormatFloat(v, 'g', -1, 64)
default:
return "nope"
}
}
Go
hello gopher
int
1
float
1
nope
Program exited.
1つ目のg
はpointer receiver methodのString()を満たしているため、最初のif文で処理されます。
n
, f
も同様にintとfloatを通ります。
最後のg
はpointerで渡されていないため、どのケースも通らずにnopeが出力されます。
より詳細なinterfaceの使い方とexampleは Effective Go に書いています。
interfaceのデータ構造
このような厳格で柔軟なinterfaceをGoはどのように実装しているのでしょうか?
ここからは実際にソースコードを読み解くことでinterfaceのデータ構造を理解します。
interfaceのデータ構造はruntimeパッケージのruntime2.goに存在します。
type iface struct {
tab *itab
data unsafe.Pointer
}
このifaceというstructはとてもシンプルで、tab、dataというフィールドを持ちます。
tabはitabというstructのポインタ型です。itabはinterfaceがラップするデータ型とinterface自体の型の詳細を定義しています。
dataはinterfaceがラップした値を示すポインタ型です。
次にitabを見てみます。itabのデータ定義も同様にruntime2.goに存在します。
// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized
}
interfacetypeをより理解できるように、先に_typeフィールドを説明します。
_typeフィールドは_type structのポインタ型です。
_typeフィールドは以下のような定義です。
// Needs to be in sync with ../cmd/link/internal/ld/decodesym.go:/^func.commonsize,
// ../cmd/compile/internal/gc/reflect.go:/^func.dcommontype and
// ../reflect/type.go:/^type.rtype.
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
_typeはGoの型を表現する様々なフィールドが定義されています。
sizeなどをはじめ、gcに必要なデータなども_typeで定義されています。
次にinterフィールドをみてみます。
itnerフィールドはinterfacetypeのポインタ型です。
interfacetypeは以下のようなデータ定義となっています。
type imethod struct {
name nameOff
ityp typeOff
}
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
interfacetypeは_typeを拡張したstructであることがわかります。
mhdrはinterfaceが保持するメソッド一覧を保持しており、このstructがinterfaceを表現しているということが理解できます。
つまり、interfaceは内部的にラップしたデータ自体のポインタとラップした値に関連するデータ構造、interfaceが保持するメソッド一覧を保持することが可能です。
以上のことから、itnerfaceがなぜ様々な値を取ることや、どのような振る舞いを持っているかということも表現できるかがわかります。
まとめ
以上でinterfaceの説明を終了します。
実際にどのようにinterfaceを介してメソッドをコールしているかなどはGo Assemblyなどのリーディングが入り、今回の説明からずれてしまう為、割愛しています。
興味のある方はぜひ読んでみることをおすすめします。
またGoのコアチームの1人で、plan9のメンバーの1人でもあるRuss Cox氏の
という記事がおすすめです。最後までお読みいただきありがとうございました。
Discussion