
こんにちは。よっしーです(^^)
本日は、Go言語の言語仕様について解説しています。
背景
Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。
言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。
そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。
言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!
プログラムの初期化
完全なプログラムを構成するパッケージ群は、一度に1つずつ、段階的に初期化されます。あるパッケージがインポートを持つ場合、インポートされたパッケージは、そのパッケージ自身が初期化される前に初期化されます。複数のパッケージが同じパッケージをインポートしている場合でも、インポートされたパッケージは一度だけ初期化されます。パッケージのインポートは、その構造上、循環的な初期化依存が存在し得ないことを保証します。より正確に言うと、次のとおりです。
インポートパスでソートされた全パッケージのリストが与えられたとき、各段階では、リストの中で未初期化のもののうち、インポートしているすべてのパッケージ(もしあれば)がすでに初期化済みである最初のパッケージが初期化されます。この段階は、すべてのパッケージが初期化されるまで繰り返されます。
パッケージの初期化、すなわち変数の初期化と init 関数の呼び出しは、単一のゴルーチンの中で、逐次的に、一度に1つのパッケージずつ行われます。init 関数は他のゴルーチンを起動することができ、それらは初期化コードと並行して実行され得ます。しかし、初期化は常に init 関数を順序立てて実行します。すなわち、前の init 関数が戻るまで、次の init 関数を呼び出すことはありません。
解説
結論:プログラム全体は、インポートされた側のパッケージから先に、1つずつ順番に初期化される。同じパッケージは何度インポートされても初期化は1回だけ。この初期化は1本の流れ(単一ゴルーチン)で、前の処理が終わってから次に進む。
前の節が「1つのパッケージ内部」の初期化順序の話だったのに対し、この節は複数のパッケージをまたいだ、プログラム全体の初期化順序の話です。
インポートされた側が先に初期化される
基本ルールはシンプルです。**「使われる側(インポートされる側)が先、使う側(インポートする側)が後」**です。
main パッケージ ──import──> shop パッケージ ──import──> math パッケージ
この場合、初期化順序は math → shop → main になります。一番奥の math から初期化され、最後に main が初期化されます。
家を建てる順序に例えると、土台(math)を先に作り、その上に壁(shop)を立て、最後に屋根(main)を載せる、というイメージです。土台がまだない状態で屋根は載せられません。インポート先(依存している相手)が準備できていないと、自分も準備できないからです。
同じパッケージは一度だけ初期化される
複数のパッケージが同じパッケージをインポートしていても、初期化は1回だけです。
shop ──import──> math
order ──import──> math
shop と order の両方が math をインポートしていますが、math の初期化(変数の初期化や init 関数)が2回走ることはありません。最初に初期化された結果が共有されます。共有の調味料を2人が使うからといって、調味料を2回作り直したりはしない、というイメージです。
循環依存は構造上あり得ない
前の節で「パッケージ内の変数」では循環依存がエラーになると説明しました。パッケージ間については、もっと根本的です。
原文の “by construction”(その構造上)が示すとおり、そもそもパッケージ同士が循環インポートすること自体がGoでは禁止されています。AがBをインポートし、BがAをインポートする、という関係はコンパイルの時点で許されません。したがってパッケージ間の初期化で堂々巡りが起きる余地はそもそもない、ということです。
初期化は「1本の流れ」で順番に進む
最後の段落が重要なポイントです。これだけ複数のパッケージや init 関数が登場しても、初期化処理そのものは単一ゴルーチンで、逐次的(1つずつ順番)に行われます。
前の節で学んだ並行処理(ゴルーチン)とは対照的に、初期化中はあちこちが同時並行で走ることはありません。1本の流れで、変数初期化 → init 関数、を1パッケージずつ片付けていきます。
ただし1つ注意点があります。init 関数の中で go someFunc() のように新しいゴルーチンを起動することは可能で、起動されたゴルーチンは初期化処理と並行して走り得ます。
func init() {
go backgroundWorker() // これは並行して走り得る
setup() // init 自体の処理は順番どおり
}
しかし、init 関数そのものの呼び出し順序は常に守られます。前の init 関数が完全に終わって戻るまで、次の init 関数は呼ばれません。リレー走者に例えると、走者(init 関数)は前の走者からバトンを受け取ってから走り出すという順序は厳守される一方で、走者が走りながら別の人(ゴルーチン)に伝言を頼むことはできる、というイメージです。
補足:この「インポート先から順に、単一ゴルーチンで逐次初期化される」という保証があるおかげで、init 関数の中では、自分がインポートしているパッケージはすべて初期化済みだと安心して使えます。たとえば main パッケージの init が走る時点では、main がインポートしているすべてのパッケージの初期化が完了している、ということです。
おわりに
本日は、Go言語の言語仕様について解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)

コメント