
こんにちは。よっしーです(^^)
本日は、Go言語の言語仕様について解説しています。
背景
Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。
言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。
そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。
言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!
プログラムの実行
完全なプログラムは、main パッケージと呼ばれる、どこからもインポートされない単一のパッケージを、それが推移的にインポートするすべてのパッケージとリンクすることで作られます。main パッケージは、パッケージ名が main でなければならず、引数を取らず値を返さない main という関数を宣言しなければなりません。
func main() { … }
プログラムの実行は、プログラムを初期化し、続いて main パッケージ内の関数 main を呼び出すことで始まります。その関数の呼び出しが戻ったとき、プログラムは終了します。プログラムは、他の(main 以外の)ゴルーチンの完了を待ちません。
解説
結論:プログラムの出発点は main パッケージの main 関数。Goはまず全体を初期化し、次に main() を呼び、main() が終わった瞬間にプログラム全体を終了する。このとき、まだ動いている他のゴルーチンを待たずに打ち切る。
プログラムの入り口は main パッケージの main 関数
Goのプログラムを実行可能にするには、次の2つが必須です。
第一に、パッケージ名が main であること。第二に、その中に main という名前の関数があり、引数も戻り値も持たないこと。
package main
func main() {
// ここからプログラムが始まる
}
これがGoプログラムの**唯一の出発点(エントリーポイント)**です。建物の正面玄関に例えると、main 関数が「ここから入ってください」という唯一の入口で、プログラムは必ずここから動き始めます。
なお main パッケージは「どこからもインポートされない」という特徴があります。他のパッケージに部品として使われる側ではなく、すべてをまとめ上げて実行する最上位の存在だからです。
「推移的にインポートする」とは、main が直接インポートするパッケージだけでなく、そのパッケージがさらにインポートするパッケージ…と芋づる式にたどった全パッケージを指します。それらすべてをリンク(結合)して1つの実行可能なプログラムができあがります。
実行の流れ:初期化 → main()
プログラムが起動すると、Goは次の順で動きます。
- プログラム全体を初期化する(前の節で説明した、インポート先から順の初期化と
init関数の実行)。 - その後、
mainパッケージのmain関数を呼び出す。
つまり main() が呼ばれる時点では、すべてのパッケージの変数初期化と init 関数が完了しています。だから main() の中では、必要な準備がすべて整った状態でコードを書けます。
main() が終わるとプログラムも終わる
main 関数の呼び出しが戻ると、プログラム全体が終了します。ここまでは直感どおりです。
注意:他のゴルーチンの完了は待たない
最後の一文が、初心者が必ず一度はハマる重要な落とし穴です。
main が終わるとき、Goはまだ動いている他のゴルーチンが終わるのを待ってくれません。 途中だろうと容赦なく、プログラムごと打ち切られます。
具体例を見てください。
package main
import (
"fmt"
"time"
)
func main() {
go func() {
time.Sleep(1 * time.Second)
fmt.Println("ゴルーチンの処理") // これは表示されない可能性が高い
}()
fmt.Println("main の処理")
// main がここで終わる → ゴルーチンを待たずに終了
}
このプログラムは「main の処理」だけ表示して終わります。起動したゴルーチンは1秒待ってから出力しようとしますが、その前に main() が終わってしまうため、プログラムごと終了し、ゴルーチンの出力は実行されません。
電車の発車に例えると、運転士(main 関数)が業務を終えて電車を車庫に入れると、まだホームで準備している乗客(他のゴルーチン)がいても、電車は待たずに終わってしまう、というイメージです。
この挙動を理解していないと、「ゴルーチンで起動した処理がなぜか実行されない」というバグに悩まされます。これを防ぐには、main がゴルーチンの完了を待つ仕組みが必要で、Goでは sync.WaitGroup やチャネルを使った待ち合わせがよく用いられます。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1) // 待つゴルーチンの数を登録
go func() {
defer wg.Done() // 終わったら完了を通知
fmt.Println("ゴルーチンの処理")
}()
wg.Wait() // すべて終わるまで待つ
fmt.Println("main の処理")
}
sync.WaitGroup は「あと何人の作業が終わるのを待つか」を数えるカウンターのような仕組みで、wg.Wait() がそのカウントがゼロになるまで main を待たせます。これで先ほどの取りこぼしを防げます。
補足:main() の中で明示的にプログラムを終了させたい場合は os.Exit() を呼ぶ方法もありますが、その場合は defer(後始末の予約)が実行されないなどの違いがあります。通常は main 関数が自然に最後まで到達して戻ることでプログラムを終えるのが基本です。sync.WaitGroup やチャネルによるゴルーチンの待ち合わせは、並行処理を扱ううえで頻出のパターンなので、別途しっかり学ぶ価値があります。
おわりに
本日は、Go言語の言語仕様について解説しました。

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

コメント