
こんにちは。よっしーです(^^)
本日は、Go言語のよくある質問 について解説しています。
背景
Go言語を学んでいると「なんでこんな仕様になっているんだろう?」「他の言語と違うのはなぜ?」といった疑問が湧いてきませんか。Go言語の公式サイトにあるFAQページには、そんな疑問に対する開発チームからの丁寧な回答がたくさん載っているんです。ただ、英語で書かれているため読むのに少しハードルがあるのも事実で、今回はこのFAQを日本語に翻訳して、Go言語への理解を深めていけたらと思い、これを読んだ時の内容を備忘として残しました。
デザイン
なぜスレッドではなくgoroutineなのですか?
Goroutineは並行処理を使いやすくすることの一部です。しばらく前からあるアイデアは、独立して実行される関数(コルーチン)を一組のスレッド上に多重化することです。コルーチンがブロッキングシステムコールを呼び出すなどしてブロックされると、ランタイムは同じオペレーティングシステムスレッド上の他のコルーチンを異なる実行可能なスレッドに自動的に移動させ、それらがブロックされないようにします。プログラマーはこれを一切見ることがなく、それがポイントです。結果として、私たちがgoroutineと呼ぶものは非常に安価になり得ます:それらはスタック用のメモリを超えるオーバーヘッドがほとんどなく、それはわずか数キロバイトです。
スタックを小さく保つために、Goのランタイムはサイズ変更可能な制限付きスタックを使用します。新しく作られたgoroutineには数キロバイトが与えられ、これはほぼ常に十分です。十分でない場合、ランタイムはスタックを保存するメモリを自動的に拡張(および縮小)し、多くのgoroutineが適度な量のメモリで動作できるようにします。CPUオーバーヘッドは関数呼び出しあたり平均約3つの安価な命令です。同じアドレス空間で数十万のgoroutineを作成することが実用的です。もしgoroutineが単なるスレッドだった場合、システムリソースははるかに少ない数で枯渇するでしょう。
解説
この節では、Go言語がなぜ従来のスレッドではなくGoroutineという独自の並行処理モデルを採用したのかを、技術的な詳細と実用性の観点から説明しています。
従来のスレッドの制限
OSスレッドの重いオーバーヘッド
// 従来のスレッド(Java/C++など)の問題
// - 各スレッドで1-8MBのスタック(固定サイズ)
// - OSカーネルでの管理オーバーヘッド
// - コンテキストスイッチのコスト
// - 数千スレッドで性能劣化
// 実際の制約例
// 1GBメモリ ÷ 2MBスタック = 約500スレッドが限界
スレッド管理の複雑さ
// Java での従来のスレッド管理(複雑)
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
final int taskId = i;
executor.submit(() -> {
try {
processTask(taskId);
} catch (Exception e) {
// エラー処理
}
});
}
executor.shutdown();
Goroutineの革新的なアプローチ
軽量な実装
// Goroutine の驚くほどシンプルな作成
func main() {
for i := 0; i < 100000; i++ {
go func(id int) {
processTask(id)
}(i)
}
time.Sleep(5 * time.Second) // 処理完了待ち
}
// メモリ使用量
// - 初期スタック: 2KB(vs スレッドの1-8MB)
// - CPU オーバーヘッド: 関数呼び出し3命令分
// - 作成コスト: ほぼゼロ
M:N スケジューリングモデル
Goroutine スケジューラの仕組み
OSスレッド(少数) M個 of OSスレッド
↑
スケジューラ
↓
Goroutines(多数) N個 of Goroutines(N >> M)
自動的なブロッキング処理
func main() {
// 1000個のGoroutineがファイルI/Oを実行
for i := 0; i < 1000; i++ {
go func(id int) {
// ファイル読み込み(ブロッキング操作)
data, err := ioutil.ReadFile(fmt.Sprintf("file%d.txt", id))
if err != nil {
return
}
// データ処理(CPU集約的処理)
result := processData(data)
fmt.Printf("Task %d completed: %v\n", id, result)
}(i)
}
time.Sleep(10 * time.Second)
}
// ランタイムの自動処理:
// 1. ファイルI/O中 → Goroutineが自動的に他のOSスレッドに移動
// 2. I/O完了 → 利用可能なOSスレッドで実行再開
// 3. プログラマーは何も意識する必要なし
動的スタック管理
スタックの自動拡張・縮小
func recursiveFunction(depth int) {
if depth == 0 {
return
}
// 大きな配列をスタックに確保
var largeArray [1024]int
largeArray[0] = depth
recursiveFunction(depth - 1) // 再帰呼び出し
}
func main() {
go func() {
// 初期スタック: 2KB
recursiveFunction(100) // スタックが自動拡張
// 関数終了後: スタックが自動縮小
}()
time.Sleep(1 * time.Second)
}
スタック管理の詳細
- 初期サイズ: 2KB(十分小さい)
- 拡張: 必要に応じて自動的に倍増
- 縮小: 未使用領域を自動的に解放
- セグメント化: 連続メモリ不要(従来スレッドと異なる)
実用的な性能比較
スループットの向上
// Web サーバーでの比較例
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 各リクエストを個別のGoroutineで処理
go func() {
// データベースアクセス(ブロッキング)
data, err := database.Query("SELECT * FROM users")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// API 呼び出し(ブロッキング)
apiResult, err := callExternalAPI(data)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// レスポンス送信
json.NewEncoder(w).Encode(apiResult)
}()
}
// 性能特性:
// - 数万の同時リクエスト処理可能
// - メモリ使用量: 数十万Goroutine でも数百MB
// - CPUコア数に関係なくスケール
メモリ効率の実測
func benchmarkGoroutines() {
var wg sync.WaitGroup
start := time.Now()
// 100万のGoroutineを作成
for i := 0; i < 1000000; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
time.Sleep(1 * time.Millisecond) // 軽い処理
}(i)
}
wg.Wait()
fmt.Printf("100万Goroutine実行時間: %v\n", time.Since(start))
// 結果例:
// - 実行時間: 約1-2秒
// - メモリ使用量: 約2GB(2KB × 100万)
// - 従来スレッドでは不可能(数十TBのメモリが必要)
}
実際の使用場面
マイクロサービス間通信
func aggregateData(userID int) (*AggregatedData, error) {
var wg sync.WaitGroup
var mu sync.Mutex
result := &AggregatedData{}
// 複数のサービスから並行してデータ取得
services := []string{"profile", "orders", "recommendations", "analytics"}
for _, service := range services {
wg.Add(1)
go func(serviceName string) {
defer wg.Done()
data, err := callService(serviceName, userID)
if err != nil {
log.Printf("Service %s failed: %v", serviceName, err)
return
}
mu.Lock()
result.merge(serviceName, data)
mu.Unlock()
}(service)
}
wg.Wait()
return result, nil
}
リアルタイム処理
func processWebSocketConnections() {
connections := make(map[string]*websocket.Conn)
for {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
continue
}
// 各接続を個別のGoroutineで処理
go func(ws *websocket.Conn) {
defer ws.Close()
for {
var message Message
err := ws.ReadJSON(&message)
if err != nil {
break
}
// メッセージを他の接続にブロードキャスト
broadcastMessage(message, connections)
}
}(conn)
}
}
Go言語の設計哲学の実現
「プログラマーは何も意識しない」 この言葉が示すGo言語の重要な設計原則:
- 複雑さの隠蔽: ランタイムが面倒な処理を自動化
- シンプルなAPI:
go
キーワード一つで並行処理 - 予測可能な動作: パフォーマンスが一貫している
実用性の追求
- 数十万Goroutine: 実際のワークロードで証明済み
- 低レイテンシ: ネットワークサービスに最適
- 高スループット: CPU集約的処理にも対応
この革新により、Go言語は「並行処理が難しい」という従来の常識を覆し、日常的に使える技術として並行プログラミングを普及させました。
おわりに
本日は、Go言語のよくある質問について解説しました。

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