Goランタイムが「動き始める瞬間」を覗く
はじめに
func main() {
fmt.Println("hello world")
}
func main() が実行される前、Go
プログラムはすでに密かにランタイムの世界を立ち上げています。
Gmain はこの混沌とした世界に送り込まれた最初の「エンジニア」です、スケジューラの起動、GC(ガーベジコレクション)の準備、パッケージ初期化を担当し、最終的に制御を
main() に引き渡す使命を持っています。
本記事では、「hello world」が実行される前に何が起きているのか、Goランタイムの起動プロセスを遡って追っていきます。
起動とスケジューラ初期化
Go プログラムの最も低レベルなエントリポイントは、アセンブリで実装された _rt0_amd64() です。
以下の初期化処理を行います。
- スタックの初期化
- TLS(スレッドローカルストレージ)の設定
- 引数の受け渡し
その後、runtime.rt0_go()
が呼び出され、Go層のランタイム処理に入ります。
この時点で Go はスケジューラの中核をなす3つの構造体を生成します:
-
M(Machine):OSスレッド。最初の M は
m0(メインスレッド) -
G(Goroutine):実行単位。最初に
g0(システムgoroutine)と
Gmain(最初のユーザーgoroutine)が生成される - P(Processor):論理プロセッサ。スケジューラキューとタイマーを保持
runtime.main() の初期化フロー解析
runtime.main() は Gmain
が実行する最重要関数であり、ユーザーコードを動かすために必要なすべての準備がここで行われます。
func main() {
// 現在の Gmain が属する M を取得
mp := getg().m
mp.g0.racectx = 0
// スタック最大値設定(64bitで1GB、32bitで250MB)
if goarch.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
// 安全上限。スタックが過剰に拡張して異常を起こすのを防ぐためです
maxstackceiling = 2 * maxstacksize
// ユーザーコード実行の準備開始フラグ
mainStarted = true
// システムモニタスレッド起動(GC、スケジューラ維持、タイマー管理など)
if haveSysmon {
systemstack(func() {
newm(sysmon, nil, -1)
})
}
// Gmain を M0 に固定
lockOSThread()
// M0 以外ならエラー
if mp != &m0 {
throw("runtime.main not on m0")
}
// 起動時間記録
runtimeInitTime = nanotime()
if runtimeInitTime == 0 {
throw("nanotime returning zero")
}
// トレース機能の有効化確認
if debug.inittrace != 0 {
inittrace.id = getg().goid
inittrace.active = true
}
// runtime パッケージ内部の初期化(メモリアロケータ、スケジューラ、GMP構造体など)
doInit(runtime_inittasks)
// 終了時にスレッドロックを解除するよう defer 設定
defer func() {
unlockOSThread()
}()
// GCを有効化
gcenable()
// 初期化完了同期用チャネル
main_init_done = make(chan bool)
// すべてのモジュールデータを走査し、各パッケージの init() を実行
for m := &firstmoduledata; m != nil; m = m.next {
doInit(m.inittasks)
}
// 初期化完了を通知
close(main_init_done)
// スレッドロック解除
unlockOSThread()
// ユーザーの main 関数を呼び出す
fn := main_main
fn()
// 終了フック(defer や os.Exit 前の処理)を実行
runExitHooks(0)
// プロセス終了
exit(0)
// 安全策。ここに到達したら panic
for {
var x *int32
*x = 0
}
}
主要フェーズの概要
-
グローバル設定:
maxstacksize、mainStarted - sysmon 起動:GC・スケジューラ・タイマーのバックグラウンド管理
-
runtime 初期化:
doInit(runtime_inittasks) -
GC 有効化:
gcenable() - 全パッケージの init() 実行
- ユーザー main() 実行
- 終了処理とプロセス終了
_start → rt0_go → schedinit → newproc(Gmain) → runtime.main → user main.main
まとめ
Gmain は Go プログラムの起動過程において、最も重要な節目のひとつです。
ランタイムの初期化(メモリアロケータ、スケジューラ、GC、M0/P0/G0
の構築)が完了すると、システムは Gmain を生成し、runtime.main() の実行を任せます。
ここから高レベルの初期化が始まり、ユーザーコードへと制御が渡ります。
言い換えれば、Gmain は Go ランタイム世界の境界点とも言えます。
その前はランタイムの準備段階、その後はユーザーロジックの実行段階。
この設計により、Go プログラムは完全に制御された環境下で初期化を行い、すべての
goroutine、メモリ、スケジューラが整合性の取れた状態で動作を開始します。
これこそが、Go
が高速な起動と安定した並行処理を実現できる根幹の理由の一つなのです。

