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向けデバッガを使ったほうがよさそうです。
GDBでGo言語のスライスの値をのぞいてみた
最近Go言語をよく触っています。触っているうちに、スライス、文字列、インターフェースなどの値がメモリ上でどう表現されているか知っておくと実装やデバッグ時に役立ちそうだなーと思うことが度々でてきました。そこでGo言語本体のソースコードを読みつつ、そのあたりをGDBでみてみました。そのときのメモです。今回はまず、スライスと文字列についてです。
使用するデバッガ
そもそもGo言語のデバッガについてよく知らなかったので、どんなのがあるか調べてみました。有名なのは次の3つのようです。
- godebug
- delve
- GDB
機能的にはdelveが一番リッチな感じでしたが、今回は簡単にメモリを覗けそうなGDBにしました。
ちなみにOSはMac OS X 64bit、Goのバージョンは1.6.2です。
スライスの値
スライスは次のような構造体をもっています(ソース)。
type slice struct { array unsafe.Pointer len int cap int }
array
は配列へのポインタ、len
はスライスが保持している値のリストの長さ、cap
は(array
が指すアドレスを始点とした)配列の長さです。len
とcap
の違いについてはGo Blogを読むのがいいかと思います。
スライスの値をGDBでみるにあたり、シンプルな例として次のようなコードを使います。
package main func main() { s := []int{1, 2, 3} _ = s }
GoのバイナリをGDBから扱いやすくするため、次のようにgcflagsオプションをつけてビルドします。
go build -gcflags "-N -l" slice1.go
GDBを起動して、スライスの値をみてみます。// 以降の文字は後で書き足しています。
% gdb ./slice1 ...省略... (gdb) break main.main // main関数にbreakpointを設置 Breakpoint 1 at 0x2040: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice1.go, line 3. (gdb) run // 実行 Starting program: /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice1 [New Thread 0x1313 of process 64589] [New Thread 0x1403 of process 64589] [New Thread 0x1503 of process 64589] Breakpoint 1, main.main () at /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice1.go:3 3 func main() { // breakpointで止まった行 (gdb) next // 次の行へ 4 s := []int{1, 2, 3} (gdb) next // 次の行へ 6 } // 今はこの行の実行前 (gdb) print &s // 変数sのアドレスを確認 $1 = (struct []int *) 0xc820035f30 (gdb) x/3gx 0xc820035f30 // 変数sの値を確認(1) 0xc820035f30: 0x000000c820035f10 0x0000000000000003 0xc820035f40: 0x0000000000000003 (gdb) x/3gx 0xc820035f10 // arrayの中身を確認 0xc820035f10: 0x0000000000000001 0x0000000000000002 0xc820035f20: 0x0000000000000003
(1)の結果から、変数sに配列へのポインタ、長さ、キャパシティが格納されているのがわかります。
スライスを関数の引数にする
次に、関数の引数にスライスを与えたとき、スライスがどうなるか確認してみます。次のコードを使います。
package main func add4(s []int) []int { s = append(s, 4) return s } func main() { s := []int{1, 2, 3} s = add4(s) }
ビルドして、GDBを起動します。
% gdb ./slice2 ...省略... (gdb) break main.main // main関数にbreakpointを設置 Breakpoint 1 at 0x2170: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2.go, line 8. (gdb) run // 実行 Starting program: /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2 [New Thread 0x1313 of process 68743] [New Thread 0x1403 of process 68743] [New Thread 0x1503 of process 68743] Breakpoint 1, main.main () at /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2.go:8 8 func main() { // breakpointで止まった行 (gdb) next // 次の行へ 9 s := []int{1, 2, 3} (gdb) next // 次の行へ 10 s = add4(s) // 今はこの行の実行前 (gdb) print &s // main関数内の変数sのアドレスを確認 $1 = (struct []int *) 0xc820035f30 (gdb) step // add4関数に入る main.add4 (s=..., ~r1=...) at /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2.go:3 3 func add4(s []int) []int { (gdb) next // 次の行へ 4 s = append(s, 4) (gdb) next // 次の行へ 5 return s // 今はこの行の実行前 (gdb) print &s // add4関数内の変数sのアドレスを確認 $3 = (struct []int *) 0xc820035ee0 (gdb) x/3gx 0xc820035ee0 // add4関数内の変数sの値を確認(3) 0xc820035ee0: 0x000000c82000e060 0x0000000000000004 0xc820035ef0: 0x0000000000000006 (gdb) x/3gx 0xc820035f30 // main関数内の変数sの値を確認(4) 0xc820035f30: 0x000000c820035f10 0x0000000000000003 0xc820035f40: 0x0000000000000003
ポイントとしては、add4関数内の変数sの値(3)と、main関数内の変数sの値(4)が異なっているところです。add4関数内の変数sは、appendによりlen
が4になっています。また、append時に配列の再確保が行われ、cap
とarray
の値も変わっています。一方で、main関数内の変数sはそのままです。ここから、関数の引数にスライスを与えると、スライスの値はコピーされることがわかります。
文字列の値
ついでに文字列の値もみてみます。文字列は次のような構造体をもっています(ソース)。
type stringStruct struct { str unsafe.Pointer len int }
str
は配列へのポインタ、len
は配列の長さです。
今度は次のコードを使います。
package main func main() { str := "Hello World" _ = str }
ビルドして、GDBで見てみます。
% gdb ./string (gdb) break main.main // main関数にbreakpointを設置 Breakpoint 1 at 0x2040: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/string.go, line 3. (gdb) run // 実行 Starting program: /Users/yagami/src/go-lib/src/github.com/ks888/hello/string [New Thread 0x1313 of process 74658] [New Thread 0x1403 of process 74658] [New Thread 0x1503 of process 74658] Breakpoint 1, main.main () at /Users/yagami/src/go-lib/src/github.com/ks888/hello/string.go:3 3 func main() { // breakpointで止まった行 (gdb) next // 次の行へ 4 str := "Hello World" (gdb) next // 次の行へ 6 } (gdb) print &str // 変数strのアドレスを確認 $2 = (struct string *) 0xc82003bf38 (gdb) x/2gx 0xc82003bf38 // 変数strの値を確認 0xc82003bf38: 0x0000000000071ca0 0x000000000000000b (gdb) x/2gx 0x0000000000071ca0 // 変数str内のポインタの値を確認 0x71ca0 <go.string.*+8024>: 0x6f57206f6c6c6548 0x0000000000646c72
変数strの値をみると、配列へのポインタ(0x0000000000071ca0
)と、配列の長さ(0x000000000000000b
)が格納されています。また、ポインタの指す先をみてみると、リトルエンディアンな上16進数で見づらいですが、Hello World
の文字列が確認できます。
感想
こういう風に理解しておくと、スライスで迷いにくくなるのでは、と期待しています。次はインターフェースがメモリ上でどう表現されているかについて書こうと思います。
参考にした記事
- http://research.swtch.com/godata Go開発者のRuss CoxによるGo言語のデータ構造解説
- https://golang.org/doc/gdb Go言語のドキュメント内にあるGDBの使い方解説
- https://blog.golang.org/go-slices-usage-and-internals スライスの内部構造解説
StackdriverでGCP上のサービスを監視してみた
今関わっているサービスは、GCPのGoogle App Engine (GAE) とGoogle Container Engine (GKE) を使って開発しています。そのサービスの監視にStackdriver Monitoringを使ってみたときのメモです。
現在の印象としては、かなり面白い機能はあるけど、基本的に思えることがうまくできないことがある、なかなか癖のある監視サービスという感じです。いくつかの機能について思ったことを以下に書いておきます。
Uptime Checks
Uptime ChecksはStackdriverの各種機能の中でも割とちゃんと使える機能だと感じました。
大抵の監視サービスだとUptime Checksのチェック先はURLで指定すると思いますが、StackdriverではGAEのModuleやグループで指定できます。下の図は(ぼかしでやや分かりづらいですが)GAEのModuleを選んでいるところです。
細かな設定として、文字列マッチングによる成否条件の設定や、任意のHTTP Header付与もできます。
デフォルト設定でも6リージョンからチェックしてもらえるのは驚きました。まぁ6リージョンいるかは分かりませんが。笑
Pingdomのような外形監視が売りのサービスに比べるとトランザクショナルな監視がないとかありますが、基本的な機能は揃っており、十分使えるレベルだと思います。
リソースの監視
リソース監視は、最初から色々設定されていて助かりましたが、結構ハマりどころがありました。
ある程度のリソース監視は最初から設定されています。特にGAEについては、結構充実しています。まぁPaaSですしね。例えば以下の項目です。
- Response Codeごとのカウント
- Response LatencyのAverageと95th percentile
- CPU使用量
- メモリの使用量
- Datastoreの各種操作回数
また、以下のようなグラフも最初から設定されています。以下はHTTP関連のグラフです。
一方で、最初から設定されているグラフにデータが全く来ない、ドキュメントの手順どおりに操作しても監視エージェントをインストールできないなど、ハマりどころもかなりありました。
特に、GKEが起動・管理するGCEインスタンスに、監視エージェントを自動インストールする方法は今でもよく分かりません。手動ならできるのですが。。そもそも、そこは監視しなくていいということなんでしょうか。。
また、相対値をグラフで見たいときにどうするのか、よくわかりませんでした。例えばメモリ使用量ではなくメモリ使用率を見たい場合とか、HTTPリクエストが5XXを返す割合を見たい場合です。収集した値に対する演算は、合計、平均、95th Percentileなど色々ありますが、相対値を見るための方法は見つかりませんでした。
リソース監視については、機能あるいはドキュメントがやや不足気味で、ちょっと使いにくい部分があると思います。
アラート
アラートは設定が大変だったので、正直あまり使いたくないです。
機能不足なわけではないです。機能に関しては、AWSのCloudWatchとかと比較してもそれほど遜色ないと思っています。アプリケーションのログにフィルターをかけてメトリクスとアラートを作成したり、複数のリソースからの値を集約したりできます。以下のように、結構色々な集約方法を選べます。
また、アラートを鳴らす条件を複数設定できるので、例えば、特定のパスにリクエストが来ているときはアラートを鳴らさない、みたいな設定もできます。まぁあまりこだわった条件にすると、メンテが大変そうですが。
アラートで大変だったのは、アラート作成のAPIが見当たらなかったことと、(全GAE Moduleなどの)グループに対してまとめてアラートを設定する機能がうまく動かなかったことです。その結果、個別のGAE Moduleごとに複数のアラートを手動で設定することになってしまいました。当然、時間がかかりますし、設定ミスもありますし、バージョン管理もできません。
さらに、作成したアラートを保存しようとすると「Failed to Save Policy. Please try again or contact support.」と言われ、何度やっても保存できなくなるバグもありました。諦めて最初からアラートを作りなおすとうまく行きます。
このように設定作業で苦しんだので、アラートは僕の中ではあまり使いたくない機能になっています。こうすると楽だよーみたいなのあれば教えて下さい!
感想
まだβですし、ドキュメントも公開されているノウハウもかなり不足していますし、バグじゃないか?と思える挙動もあります。でも、GAEとの相性はいいし、面白い機能もあるので、余裕があれば使ってみるのもありだと思います。