🐁

Goのinterfaceをデータ構造から理解する

5 min read

この記事は 2021 Go Advent Calendar 3日目 の記事です。

はじめに

Goにはinterfaceと呼ばれるデータ構造が存在します。
interface typeは良くも悪くもGoのランタイムの中で柔軟な振る舞いを持ちます。
今回は改めてGoのinterfaceを学習することで、今日からのGopher lifeをより良くすることを目的とします。

note

この記事はGo1.17の時点での記事です。
Go1.18から導入予定のGenericが導入されることで、一部よりいい書き方が生まれる可能性があります。

interfaceの使い方

1つは他言語の文脈で用いられるような抽象化のためのinterfaceです。
以下がサンプルコードです。

https://go.dev/play/p/W2FfTvlXQvh
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 を渡してしまうなどのミスを防ぐことが可能です。

また以下のようなコードがあるとします。

https://go.dev/play/p/xtJ0TJk6h6v
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を持つあらゆる型を受け入れるといったことも可能です。

そして、先ほど述べたランタイムの中で動的に振る舞いを変えることも可能です。

https://go.dev/play/p/8jXj3Evo8AO
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に存在します。

https://github.com/golang/go/blob/a563954b799c6921fc3666b4723d38413f442145/src/runtime/runtime2.go#L143-L146
type iface struct {
	tab  *itab
	data unsafe.Pointer
}

このifaceというstructはとてもシンプルで、tab、dataというフィールドを持ちます。
tabはitabというstructのポインタ型です。itabはinterfaceがラップするデータ型とinterface自体の型の詳細を定義しています。

dataはinterfaceがラップした値を示すポインタ型です。

次にitabを見てみます。itabのデータ定義も同様にruntime2.goに存在します。

https://github.com/golang/go/blob/a563954b799c6921fc3666b4723d38413f442145/src/runtime/runtime2.go#L623-L636
// 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フィールドは以下のような定義です。

https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/type.go#L25-L43
// 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は以下のようなデータ定義となっています。

https://github.com/golang/go/blob/a563954b799c6921fc3666b4723d38413f442145/src/runtime/type.go#L337-L346
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氏の

https://research.swtch.com/interfaces
という記事がおすすめです。

最後までお読みいただきありがとうございました。

Discussion

ログインするとコメントできます