読者です 読者をやめる 読者になる 読者になる

Go言語のインターフェースの値は、メモリ上でどう表現されているか

Go

前回の記事では、Go言語のスライスと文字列の値がメモリ上でどう表現されているか見てみました。今回はインターフェースへの理解を深めるべく、インターフェースの値がメモリ上でどう表現されているかGDBでみてみます。

インターフェースの値

まず、インターフェースの構造体は次のようになっています(runtime/runtime2.go#L71)。

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

itabは、インターフェースの型情報や、メソッドの配列等を保持する構造体です。この構造体の定義は、後でGDBを動かすときに確認します。dataはインターフェースを実装した構造体へのポインタを保持しています。

早速、実際の値をGDBで見てみます。今回は例として次のようなコードを使います。環境は前回と同じく、OSがMac OS X 64bit、Goのバージョンが1.6.2です。

package main

import (
  "fmt"
)

type Calc interface {
  Add() int
  Mul() int
}

type Nums struct {
  a, b int
}

func (ns *Nums) Add() int {
  return ns.a + ns.b
}

func (ns *Nums) Mul() int {
  return ns.a * ns.b
}

func Print(c Calc) {
  fmt.Println(c.Add())
  fmt.Println(c.Mul())
}

func main() {
  ns := &Nums{1, 2}
  Print(ns)
}

GoのバイナリをGDBから扱いやすくするため、次のようにgcflagsオプションをつけてビルドします。

go build -gcflags "-N -l" interface.go

GDBを起動して、インターフェースの値をみてみます。// 以降の文字は後で書き足しています。

% gdb ./interface
...省略...
(gdb) break main.main  // main関数にbreakpointを設置
Breakpoint 1 at 0x2340: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/interface.go, line 29.
(gdb) break main.Print  // Print関数にbreakpointを設置
Breakpoint 2 at 0x20c0: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/interface.go, line 24.
(gdb) run  // 実行
Starting program: /Users/yagami/src/go-lib/src/github.com/ks888/hello/interface
[New Thread 0x1313 of process 1307]
[New Thread 0x1403 of process 1307]
[New Thread 0x1503 of process 1307]

Breakpoint 1, main.main () at /Users/yagami/src/go-lib/src/github.com/ks888/hello/interface.go:29
29      func main() {  // breakpointで止まった行
(gdb) next  // 次の行へ
30        ns := &Nums{1, 2}
(gdb) next  // 次の行へ
31        Print(ns)  // 今はこの行の実行前
(gdb) print ns  // 変数ns(*Nums)の値を確認(1)
$2 = (struct main.Nums *) 0xc82000a300
(gdb) x/2gx 0x000000c82000a300  // *ns(Nums)の値を確認
0xc82000a300:   0x0000000000000001      0x0000000000000002
(gdb) continue  // 実行を続ける
Continuing.

Breakpoint 2, main.Print (c=...) at /Users/yagami/src/go-lib/src/github.com/ks888/hello/interface.go:24
24      func Print(c Calc) {  // breakpointで止まった行
(gdb) next  // 次の行へ
25        fmt.Println(c.Add())
(gdb) print &c  // 変数c(*Calc)のアドレスを確認
$3 = (main.Calc *) 0xc820035ef0
(gdb) x/2gx 0xc820035ef0  // 変数c(Calc)の値を確認(2)
0xc820035ef0:   0x00000000002121c0      0x000000c82000a300

ポイントとしては、(1)で確認したNums構造体へのポインタ値0x000000c82000a300が、(2)で確認したCalc構造体の値に含まれていることです。ここから、iface構造体のdataフィールドには、Nums構造体へのポインタが格納されていることがわかります。

次は、iface構造体のtabフィールドが指すitab構造体を確認してみます。

itab構造体は次のように定義されています(runtime/runtime2.go#L514)。

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    bad    int32
    unused int32
    fun    [1]uintptr // variable sized
}

各フィールドは次のような意味です。

  • inter: インターフェースの型についての情報。
  • _type: インターフェースを実装した型についての情報。
  • link: 他のitab構造体へのポインタ。itab構造体の値をハッシュテーブルにキャッシュするときに使用。
  • bad: _typeの型がインターフェースを正しく実装できているかどうかを示します。メソッドが足りない場合はフラグが立ちます。
  • unused: たぶんアラインメント用です。
  • fun: メソッドの配列。正確には、インターフェースを実装した型がもつメソッドへのポインタの配列。この定義では長さ1の配列になっていますが、実際はメソッド数分の配列になっています。itab構造体の値を生成するときに、足りない分は補ってメモリ確保されます。

この構造体の値のうち、funフィールドの値をGDBで確認してみます。先程の続きから実行していきます。

(gdb) x/6gx 0x00000000002121c0  // tabフィールドの値を確認
0x2121c0:       0x00000000000da2e0      0x00000000000da920
0x2121d0:       0x0000000000000000      0x0000000000000000
0x2121e0:       0x0000000000002040      0x0000000000002080
(gdb) x/gx 0x0000000000002040  // メソッドの配列の1番目の値を確認(3)
0x2040 <main.(*Nums).Add>:      0x000000102444c748
(gdb) x/gx 0x0000000000002080  // メソッドの配列の2番目の値を確認(4)
0x2080 <main.(*Nums).Mul>:      0x000000102444c748

(3)、(4)から確認できる通り、配列の1番目にはNums構造体のAddメソッドへのポインタ、配列の2番目にはNums構造体のMulメソッドへのポインタが格納されています。

ちなみに今回は割愛しましたが、inter_typeフィールドについては、runtime/type.goで構造体の定義を確認できます。よければ値と合わせて確認してみてください。

インターフェース値の生成

ついでに、itab構造体のfunフィールド(メソッドの配列)がどう生成されているか確認してみます。

itab構造体の値はruntimeパッケージのtyp2Itab関数で生成されています(runtime/iface.go#L122)。

func typ2Itab(t *_type, inter *interfacetype, cache **itab) *itab {
    tab := getitab(inter, t, false)
    atomicstorep(unsafe.Pointer(cache), unsafe.Pointer(tab))
    return tab
}

getitab関数でitab構造体の値を生成しているようなので、そちらもみてみます(runtime/iface.go#L22)。日本語のコメントは僕が挿入したものです。

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    // 省略

    x := typ.x  // インターフェースを実装した型のメソッドリストを保持するuncommontype構造体

    // 省略

    var m *itab

    // 省略

    // itab構造体のメモリ確保。インターフェースのメソッド数に合わせて上乗せで確保している
    m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
    m.inter = inter
    m._type = typ

search:
    // both inter and typ have method sorted by name,
    // and interface names are unique,
    // so can iterate over both in lock step;
    // the loop is O(ni+nt) not O(ni*nt).
    ni := len(inter.mhdr)  // インターフェースのメソッド数
    nt := len(x.mhdr)  // インターフェースを実装した構造体のメソッド数
    j := 0
    for k := 0; k < ni; k++ {
        i := &inter.mhdr[k]  // インターフェースのメソッド
        iname := i.name
        ipkgpath := i.pkgpath
        itype := i._type
        for ; j < nt; j++ {
            t := &x.mhdr[j]  // インターフェースを実装した構造体のメソッド
            // 名前、型、パッケージが一致しているか調べる
            if t.mtyp == itype && (t.name == iname || *t.name == *iname) && t.pkgpath == ipkgpath {
                // 一致するメソッドが見つかったので、itab構造体にメソッドへのポインタを書き込む
                if m != nil {
                    *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = t.ifn
                }
                goto nextimethod
            }
        }
        // 一致するメソッドが見つからなかった場合の処理
        // 省略
        m.bad = 1
        break
    nextimethod:
    }

    // 省略
    return m
}

メソッドへのポインタの配列は、二重forループのところで埋めています。コードの通りですが、インターフェースのメソッドと、インターフェースを実装した構造体のメソッドをマッチングし、マッチしたら配列にメソッドへのポインタを書き込みます。

ちなみに省略しましたが、itab構造体はハッシュテーブルでキャッシュされています。なのでインターフェース値を作成する度にマッチングをする必要はありません。

感想

type *Interface is pointer to interface, not interfaceというエラーを調べていたときに見つけたGo開発者Russ Coxさんのブログ記事をかなり参考にしました。GDBで値を確認しつつGo言語のソースコードを読むと、Goの内部構造が少しみえてきて面白いです。

ちなみに今回活躍したGDBですが、Go言語ではあまりうまく動かないと言われています。実際使っていても、stepコマンドを出したらGo内部の関数に飛んでしまったりしました。実アプリでデバッガを使いたい場合には、delve等のGo向けデバッガを使ったほうがよさそうです。