
Goroutine ID を取得する方法

オペレーティングシステムにおいて、各プロセスには固有のプロセスIDがあり、各スレッドには独自のスレッドIDがあります。同様に、Go言語では、各ゴルーチンには独自のゴルーチンIDがあり、`panic`のようなシナリオでよく遭遇します。ゴルーチンには固有のIDがあるものの、Go言語は意図的にこのIDを取得するインターフェースを提供していません。今回は、Goアセンブリ言語を通じてゴルーチンIDを取得してみます。
1. `goid`を持たない公式設計(https://github.com/golang/go/issues/22770)
公式の関連資料によると、Go言語が意図的に`goid`を提供しない理由は、乱用を避けるためです。多くのユーザーは、`goid`を簡単に取得した後、後続のプログラミングで`goid`に強く依存するコードを無意識に書いてしまうからです。`goid`への強い依存は、このコードの移植を難しくし、また並行モデルを複雑化します。同時に、Go言語には膨大な数のゴルーチンが存在する可能性がありますが、各ゴルーチンが破棄されるときにリアルタイムで監視することは容易ではなく、`goid`に依存するリソースが自動的に回収されないこともあります(手動での回収が必要です)。ただし、もしあなたがGoアセンブリ言語のユーザーであれば、これらの懸念を完全に無視することができます。
注意: `goid`を強制的に取得すると、「恥をかかされる」かもしれません 😂:
https://github.com/golang/go/blob/master/src/runtime/proc.go#L7120
2. 純粋なGoでの`goid`取得
理解を助けるために、まず純粋なGoで`goid`を取得してみましょう。純粋なGoで`goid`を取得するパフォーマンスは比較的低いですが、コードの移植性は良く、他の方法で取得した`goid`が正しいかどうかをテストおよび検証するためにも使用できます。
すべてのGo言語ユーザーは`panic`関数を知っているはずです。`panic`関数を呼び出すと、ゴルーチンが例外を起こします。もし`panic`がゴルーチンのルート関数に到達する前に`recover`関数によって処理されない場合、ランタイムは関連する例外とスタック情報を出力し、ゴルーチンを終了します。
`panic`を通じて`goid`を出力する簡単な例を構築してみましょう:
package main
func main() {
panic("leapcell")
}
実行すると、以下の情報が出力されます:
panic: leapcell
goroutine 1 [running]:
main.main()
/path/to/main.go:4 +0x40
`Panic`出力情報の`goroutine 1 [running]`の中の`1`が`goid`であると推測できます。では、プログラム内で`panic`の出力情報をどう取得することができるでしょうか?実際、上記の情報は現在の関数呼び出しスタックフレームのテキスト記述に過ぎません。`runtime.Stack`関数はこの情報を取得する機能を提供しています。
`runtime.Stack`関数に基づいて例を再構築し、現在のスタックフレームの情報を出力することで`goid`を出力してみましょう:
package main
import "runtime"
func main() {
var buf = make([]byte, 64)
var stk = buf[:runtime.Stack(buf, false)]
print(string(stk))
}
実行すると、以下の情報が出力されます:
goroutine 1 [running]:
main.main()
/path/to/main.g
`runtime.Stack`で取得した文字列から`goid`情報を解析するのは簡単です:
import (
"fmt"
"strconv"
"strings"
"runtime"
)
func GetGoid() int64 {
var (
buf [64]byte
n = runtime.Stack(buf[:], false)
stk = strings.TrimPrefix(string(buf[:n]), "goroutine")
)
idField := strings.Fields(stk)[0]
id, err := strconv.Atoi(idField)
if err!= nil {
panic(fmt.Errorf("can not get goroutine id: %v", err))
}
return int64(id)
}
`GetGoid`関数の詳細については説明しません。`runtime.Stack`関数は、現在のゴルーチンのスタック情報だけでなく、すべてのゴルーチンのスタック情報も取得できることに注意してください(2番目のパラメータで制御されます)。同時に、Go言語の`net/http2.curGoroutineID`関数も同様の方法で`goid`を取得しています。
3. `g`構造体からの`goid`取得
公式のGoアセンブリ言語ドキュメントによると、各実行中のゴルーチン構造体の`g`ポインタは、現在実行中のゴルーチンが存在するシステムスレッドのローカルストレージTLSに格納されています。まずTLSスレッドローカルストレージを取得し、その後TLSから`g`構造体のポインタを取得し、最後に`g`構造体から`goid`を抽出することができます。
`runtime`パッケージで定義されている`get_tls`マクロを参照して`g`ポインタを取得するコードは以下の通りです:
get_tls(CX)
MOVQ g(CX), AX // gをAXに移動する。
`get_tls`は`runtime/go_tls.h`ヘッダーファイルで定義されたマクロ関数です。
AMD64プラットフォームの場合、`get_tls`マクロ関数は以下のように定義されています:
#ifdef GOARCH_amd64
#define get_tls(r) MOVQ TLS, r
#define g(r) 0(r)(TLS*1)
#endif
`get_tls`マクロ関数を展開すると、`g`ポインタを取得するコードは以下の通りです:
MOVQ TLS, CX
MOVQ 0(CX)(TLS*1), AX
実際、TLSはスレッドローカルストレージのアドレスに似ており、そのアドレスに対応するメモリ内のデータが`g`ポインタです。もっと直接的には:
MOVQ (TLS), AX
上記の方法に基づいて、`g`ポインタを取得する`getg`関数をラップすることができます:
// func getg() unsafe.Pointer
TEXT ·getg(SB), NOSPLIT, $0-8
MOVQ (TLS), AX
MOVQ AX, ret+0(FP)
RET
そして、Goコードでは、`g`構造体の`goid`メンバのオフセットを通じて`goid`の値を取得します:
const g_goid_offset = 152 // Go1.10
func GetGroutineId() int64 {
g := getg()
p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
return *p
}
ここで、`g_goid_offset`は`goid`メンバのオフセットです。`g`構造体は`runtime/runtime2.go`を参照してください。
Go1.10バージョンでは、`goid`のオフセットは152バイトです。したがって、上記のコードは`goid`のオフセットも152バイトのGoバージョンでのみ正しく動作します。偉大なトンプソンの神託によれば、列挙とブルートフォースはすべての難問の万能薬です。`goid`のオフセットをテーブルに保存し、その後Goバージョン番号に応じて`goid`のオフセットを照会することもできます。
以下は改善されたコードです:
var offsetDictMap = map[string]int64{
"go1.10": 152,
"go1.9": 152,
"go1.8": 192,
}
var g_goid_offset = func() int64 {
goversion := runtime.Version()
for key, off := range offsetDictMap {
if goversion == key || strings.HasPrefix(goversion, key) {
return off
}
}
panic("unsupported go version:"+goversion)
}()
これで、`goid`のオフセットは最終的にリリースされたGo言語バージョンに自動的に適応することができます。
4. `g`構造体に対応するインターフェースオブジェクトの取得
列挙とブルートフォースはシンプルですが、開発中の未リリースのGoバージョンをうまくサポートしていません。開発中の特定のバージョンでの`goid`メンバのオフセットを事前に知ることはできません。
もし`runtime`パッケージ内であれば、`unsafe.OffsetOf(g.goid)`を通じてメンバのオフセットを直接取得することができます。また、反射を通じて`g`構造体の型を取得し、その型を通じて特定のメンバのオフセットを照会することもできます。`g`構造体は内部型であるため、Goコードは外部パッケージから`g`構造体の型情報を取得することはできません。ただし、Goアセンブリ言語では、すべてのシンボルを見ることができるので、理論的には`g`構造体の型情報も取得できます。
任意の型が定義されると、Go言語はその型に対応する型情報を生成します。たとえば、`g`構造体は`type·runtime·g`識別子を生成して`g`構造体の値型情報を表し、`type·*runtime·g`識別子を生成してポインタ型情報を表します。もし`g`構造体にメソッドがあれば、`go.itab.runtime.g`と`go.itab.*runtime.g`型情報も生成されて、メソッド付きの型情報を表します。
もし`g`構造体の型を表す`type·runtime·g`と`g`ポインタを取得できれば、`g`オブジェクトのインターフェースを構築することができます。以下は改善された`getg`関数で、`g`ポインタオブジェクトのインターフェースを返します:
// func getg() interface{}
TEXT ·getg(SB), NOSPLIT, $32-16
// get runtime.g
MOVQ (TLS), AX
// get runtime.g type
MOVQ $type·runtime·g(SB), BX
// convert (*g) to interface{}
MOVQ AX, 8(SP)
MOVQ BX, 0(SP)
CALL runtime·convT2E(SB)
MOVQ 16(SP), AX
MOVQ 24(SP), BX
// return interface{}
MOVQ AX, ret+0(FP)
MOVQ BX, ret+8(FP)
RET
ここで、AXレジスタは`g`ポインタに対応し、BXレジスタは`g`構造体の型に対応します。そして、`runtime·convT2E`関数は型をインターフェースに変換するために使用されます。`g`構造体のポインタ型を使用していないため、返されるインターフェースは`g`構造体の値型を表します。理論的には、`g`ポインタ型のインターフェースも構築することができますが、Goアセンブリ言語の制限により、`type·*runtime·g`識別子を使用することはできません。
`g`が返すインターフェースに基づいて、`goid`を取得するのは簡単です:
import (
"reflect"
)
func GetGoid() int64 {
g := getg()
gid := reflect.ValueOf(g).FieldByName("goid").Int()
return gid
}
上記のコードは反射を通じて直接`goid`を取得しています。理論的には、反射されるインターフェースの名前と`goid`メンバの名前が変更されない限り、コードは正常に動作するはずです。実際のテストでは、上記のコードは、Go1.8、Go1.9、Go1.10バージョンで正しく動作することが確認されています。楽観的に見ると、もし`g`構造体の型名が変更されず、Go言語の反射メカニズムにも変更がなければ、将来のGo言語バージョンでも動作するはずです。
反射はある程度の柔軟性を持っていますが、そのパフォーマンスはいつも批判の的になっています。改善のアイデアとしては、反射を初期化段階で一回だけ実行して`goid`のオフセットを取得し、その後`g`ポインタとオフセットを使って`goid`を取得することです。
以下は`g_goid_offset`変数の初期化コードです:
var g_goid_offset uintptr = func() uintptr {
g := GetGroutine()
if f, ok := reflect.TypeOf(g).FieldByName("goid"); ok {
return f.Offset
}
panic("can not find g.goid field")
}()
正しい`goid`オフセットを得た後、以前に述べた方法で`goid`を取得します:
func GetGroutineId() int64 {
g := getg()
p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
return *p
}
ここで、`goid`を取得するための実装アイデアは十分に整っていますが、アセンブリコードには依然として深刻なセキュリティリスクがあります。
`getg`関数は`NOSPLIT`フラグでスタック分割を禁止する関数型として宣言されていますが、内部的にはより複雑な`runtime·convT2E`関数を呼び出しています。もし`runtime·convT2E`関数がスタックスペース不足に遭遇すると、スタック分割操作をトリガーする可能性があります。スタックが分割されると、GCは関数の引数、戻り値、ローカル変数内のスタックポインタを移動させます。ただし、`getg`関数はローカル変数のポインタ情報を提供していません。
以下は改善された`getg`関数の完全な実装です:
// func getg() interface{}
TEXT ·getg(SB), NOSPLIT, $32-16
NO_LOCAL_POINTERS
MOVQ $0, ret_type+0(FP)
MOVQ $0, ret_data+8(FP)
GO_RESULTS_INITIALIZED
// get runtime.g
MOVQ (TLS), AX
// get runtime.g type
MOVQ $type·runtime·g(SB), BX
// convert (*g) to interface{}
MOVQ AX, 8(SP)
MOVQ BX, 0(SP)
CALL runtime·convT2E(SB)
MOVQ 16(SP), AX
MOVQ 24(SP), BX
// return interface{}
MOVQ AX, ret_type+0(FP)
MOVQ BX, ret_data+8(FP)
RET
ここで、`NO_LOCAL_POINTERS`は関数にローカルポインタ変数がないことを意味します。同時に、返されるインターフェースはゼロ値で初期化され、初期化が完了した後、`GO_RESULTS_INITIALIZED`を使ってGCに通知します。これにより、スタックが分割されたときに、GCが戻り値とローカル変数内のポインタを正しく処理できるようになります。
5. `goid`の応用:ローカルストレージ
`goid`を使えば、ゴルーチンローカルストレージを簡単に構築することができます。`gls`パッケージを定義して`goid`機能を提供することができます:
package gls
var gls struct {
m map[int64]map[interface{}]interface{}
sync.Mutex
}
func init() {
gls.m = make(map[int64]map[interface{}]interface{})
}
`gls`パッケージ変数は単に`map`をラップし、`sync.Mutex`ミューテックスを使って並行アクセスをサポートしています。
その後、内部の`getMap`関数を定義して、各ゴルーチンの`map`を取得します:
func getMap() map[interface{}]interface{} {
gls.Lock()
defer gls.Unlock()
goid := GetGoid()
if m, _ := gls.m[goid]; m!= nil {
return m
}
m := make(map[interface{}]interface{})
gls.m[goid] = m
return m
}
ゴルーチンのプライベートな`map`を取得した後、追加、削除、変更操作の通常のインターフェースです:
func Get(key interface{}) interface{} {
return getMap()[key]
}
func Put(key interface{}, v interface{}) {
getMap()[key] = v
}
func Delete(key interface{}) {
delete(getMap(), key)
}
最後に、`Clean`関数を提供してゴルーチンに対応する`map`リソースを解放します:
func Clean() {
gls.Lock()
defer gls.Unlock()
delete(gls.m, GetGoid())
}
このように、最小限のゴルーチンローカルストレージ`gls`オブジェクトが完成します。
以下はローカルストレージを使った簡単な例です:
import (
"fmt"
"sync"
"gls/path/to/gls"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
defer gls.Clean()
defer func() {
fmt.Printf("%d: number = %d\n", idx, gls.Get("number"))
}()
gls.Put("number", idx+100)
}(i)
}
wg.Wait()
}
ゴルーチンローカルストレージを通じて、異なるレベルの関数がストレージリソースを共有することができます。同時に、リソースリークを避けるために、ゴルーチンのルート関数で`defer`文を使って`gls.Clean()`関数を呼び出し、リソースを解放する必要があります。
Leapcell: The Advanced Serverless Platform for Hosting Golang Applications
最後に、Goサービスをデプロイする最適なプラットフォームをお薦めします:leapcell

1. 多言語サポート
JavaScript、Python、Go、またはRustで開発できます。
2. 無制限のプロジェクトを無料でデプロイ
使用量に応じて支払います — リクエストがなければ、料金はかかりません。
3. 比類なきコスト効率
使った分だけ支払い、アイドル時の料金はありません。
例:25ドルで平均応答時間60msの694万件のリクエストをサポートします。
4. 簡素化された開発者体験
直感的なUIで簡単なセットアップが可能です。
完全自動化されたCI/CDパイプラインとGitOps統合。
アクション可能なインサイトを得るためのリアルタイムメトリクスとロギング。
5. 簡単なスケーラビリティと高性能
自動スケーリングで高い並列性を簡単に処理できます。
オペレーションオーバーヘッドはゼロ — ビルドに集中できます。
詳細はドキュメントを参照してください。
Leapcell Twitter: https://x.com/LeapcellHQ