初めて Go を使い始めると、main 関数はほとんど単純すぎるように思えます。エントリ ポイントは 1 つで、main.go を実行するだけで簡単に実行できます。プログラムが起動して実行されます。
しかし、さらに深く掘り下げていくと、カーテンの裏で微妙でよく考えられたプロセスが進行していることがわかりました。メインが開始される前に、Go ランタイムはインポートされたすべてのパッケージの慎重な初期化を調整し、その init 関数を実行して、すべてが正しい順序であることを確認します。面倒な予期せぬ事態は許されません。
Go の配置方法には細かい詳細があり、すべての Go 開発者が知っておくべきだと思います。これは、コードの構造、共有リソースの処理、さらにはシステムへのエラーの伝達方法に影響を与えるからです。
メインがギアを開始する前後で実際に何が起こっているのかを浮き彫りにする、一般的なシナリオと質問をいくつか見てみましょう。
これを想像してください。それぞれに独自の init 関数を持つ複数のパッケージがあります。おそらく、そのうちの 1 つはデータベース接続を構成し、もう 1 つはログのデフォルトを設定し、3 つ目はラムダ ワーカーを初期化し、4 つ目は SQS キュー リスナーを初期化します。
メインが実行されるまでに、すべての準備が整っている必要があります。中途半端な初期化状態や、土壇場で予期せぬ事態が発生することはありません。
例: 複数のパッケージと初期注文
// db.go package db import "fmt" func init() { fmt.Println("db: connecting to the database...") // Imagine a real connection here } // cache.go package cache import "fmt" func init() { fmt.Println("cache: warming up the cache...") // Imagine setting up a cache here } // main.go package main import ( _ "app/db" // blank import for side effects _ "app/cache" "fmt" ) func main() { fmt.Println("main: starting main logic now!") }
このプログラムを実行すると、以下が表示されます:
db: connecting to the database... cache: warming up the cache... main: starting main logic now!
データベースが最初に初期化され (mainimports db のため)、次にキャッシュが初期化され、最後に main がそのメッセージを出力します。 Go は、インポートされたすべてのパッケージが mainrun の前に初期化されることを保証します。この依存関係に基づいた順序が重要です。キャッシュが db に依存している場合は、キャッシュの init が実行される前に db がセットアップを完了しているはずです。
では、キャッシュの前に dbinitialized が絶対に必要な場合、またはその逆の場合はどうすればよいでしょうか?自然なアプローチは、キャッシュがデータベースに依存するか、メインのデータベースの後にインポートされるようにすることです。 Go は、main.go にリストされているインポートの順序ではなく、依存関係の順序でパッケージを初期化します。私たちが使用するトリックは、空の import: _ "path/to/package" - 特定のパッケージの初期化を強制することです。ただし、私は主な方法として空のインポートに依存しません。依存関係が不明確になり、メンテナンスの問題が発生する可能性があります。
代わりに、初期化順序が依存関係から自然に現れるようにパッケージを構造化することを検討してください。それが不可能な場合は、初期化ロジックはコンパイル時の厳密な順序付けに依存すべきではない可能性があります。たとえば、sync.Once または同様のパターンを使用して、実行時にデータベースの準備ができているかどうかをキャッシュでチェックすることができます。
パッケージ A と B の両方が共有リソース (おそらく構成ファイルまたはグローバル設定オブジェクト) に依存するシナリオを想像してください。どちらにも init 関数があり、両方ともそのリソースを初期化しようとします。リソースが 1 回だけ初期化されるようにするにはどうすればよいですか?
一般的な解決策は、共有リソースの初期化を sync.Once 呼び出しの後ろに置くことです。これにより、複数のパッケージが初期化コードをトリガーした場合でも、初期化コードが 1 回だけ実行されることが保証されます。
例: 単一の初期化を保証する
// db.go package db import "fmt" func init() { fmt.Println("db: connecting to the database...") // Imagine a real connection here } // cache.go package cache import "fmt" func init() { fmt.Println("cache: warming up the cache...") // Imagine setting up a cache here } // main.go package main import ( _ "app/db" // blank import for side effects _ "app/cache" "fmt" ) func main() { fmt.Println("main: starting main logic now!") }
これで、構成をインポートするパッケージの数に関係なく、someValue の初期化は 1 回だけ行われます。パッケージ A と B の両方が config.Value() に依存している場合、両方とも適切に初期化された値が表示されます。
同じファイル内に複数の init 関数を含めることができ、それらは出現した順序で実行されます。同じパッケージ内の複数のファイルにわたって、Go は一貫した順序で init 関数を実行しますが、厳密に定義された順序ではありません。コンパイラはファイルをアルファベット順に処理する場合がありますが、それに依存すべきではありません。コードが同じパッケージ内の init 関数の特定のシーケンスに依存している場合、それは多くの場合、リファクタリングの必要がある兆候です。 init ロジックを最小限に抑え、密結合を避けます。
正当な使用とアンチパターン
init 関数は、データベースドライバーの登録、コマンドラインフラグの初期化、ロガーのセットアップなどの単純なセットアップに最適に使用されます。複雑なロジック、長時間実行される I/O、または正当な理由なくパニックを起こす可能性のあるコードは、別の場所で処理する方が適切です。
経験則として、init で大量のロジックを作成していることに気付いた場合は、そのロジックを main で明示的に作成することを検討するとよいでしょう。
Go の main は値を返しません。外部にエラーを通知したい場合は、os.Exit() が役立ちます。ただし、os.Exit() を呼び出すとプログラムが即座に終了することに注意してください。遅延関数は実行されず、パニックスタックトレースも出力されません。
例: 終了前のクリーンアップ
db: connecting to the database... cache: warming up the cache... main: starting main logic now!
クリーンアップ呼び出しをスキップして os.Exit(1) に直接ジャンプすると、リソースを適切にクリーンアップする機会を失います。
パニックによってプログラムを終了することもできます。遅延関数のrecover()によって回復されないパニックが発生すると、プログラムがクラッシュし、スタック・トレースが出力されます。これはデバッグには便利ですが、通常のエラー信号には理想的ではありません。 os.Exit() とは異なり、パニックはプログラムが終了する前に遅延関数を実行する機会を与えます。これはクリーンアップに役立ちますが、クリーンな終了コードを期待するエンドユーザーやスクリプトにとっては整頓されていないように見える可能性もあります。
シグナル (Cmd C からの SIGINT など) によってもプログラムを終了できます。あなたが兵士であれば、信号をキャッチし、適切に処理することができます。
初期化はゴルーチンが起動される前に行われ、起動時に競合状態が発生しないようにします。ただし、メインが開始されると、好きなだけゴルーチンをスピンアップできます。
main 関数自体は、Go ランタイムによって開始される特別な「メイン goroutine」で実行されることに注意することが重要です。 main が返されると、他の goroutine がまだ動作している場合でも、プログラム全体が終了します。
これはよくある落とし穴です。バックグラウンドのゴルーチンを開始したからといって、プログラムが存続するわけではありません。メインが終了すると、すべてがシャットダウンします。
// db.go package db import "fmt" func init() { fmt.Println("db: connecting to the database...") // Imagine a real connection here } // cache.go package cache import "fmt" func init() { fmt.Println("cache: warming up the cache...") // Imagine setting up a cache here } // main.go package main import ( _ "app/db" // blank import for side effects _ "app/cache" "fmt" ) func main() { fmt.Println("main: starting main logic now!") }
この例では、main が終了する前に 3 秒待機するため、ゴルーチンはメッセージのみを出力します。 main が早く終了した場合、プログラムは goroutine が完了する前に終了します。ランタイムは、メインが終了するときに他のゴルーチンを「待機」しません。ロジックで特定のタスクが完了するのを待つ必要がある場合は、WaitGroup やチャネルなどの同期プリミティブを使用して、バックグラウンド作業が完了したことを通知することを検討してください。
初期化中にパニックが発生すると、プログラム全体が終了します。メインもなければ回復機会もない。デバッグに役立つパニック メッセージが表示されます。これが、私が init 関数をシンプルかつ予測可能に保ち、予期せず爆発する可能性のある複雑なロジックを排除しようとする理由の 1 つです。
メインが実行されるまでに、Go は目に見えない大量の作業をすでに行っています。すべてのパッケージが初期化され、すべての init 関数が実行され、厄介な循環依存関係が潜んでいないかチェックされます。このプロセスを理解すると、アプリケーションの起動シーケンスをより詳細に制御し、自信を持って実行できるようになります。
何か問題が発生したときに、きれいに終了する方法と、遅延関数に何が起こるかを知っています。コードがより複雑になると、ハッキングに頼らずに初期化順序を強制する方法がわかります。そして、同時実行性が関係する場合、競合状態は init 実行前ではなく、init 実行後に始まることがわかります。
私にとって、これらの洞察により、Go の一見単純な main 関数がエレガントな氷山の一角のように感じられました。あなた自身のトリック、つまずいた落とし穴、またはこれらの内部構造に関する質問がある場合は、ぜひお聞きください。
結局のところ、私たちは皆まだ学習中です - そしてそれが Go 開発者になる楽しみの半分です。
読んでいただきありがとうございます!コードがあなたと一緒にありますように:)
私のソーシャル リンク: LinkedIn |ギットハブ | ? (旧Twitter) |サブスタック |開発者
さらに詳しい内容については、以下をご検討ください。またね!
以上がGo のエントリーポイントの裏側を覗く - 初期化から終了までの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。