Go言語入門:よくある質問 -デザイン Vol.9-

スポンサーリンク
Go言語入門:よくある質問 -デザイン Vol.9- ノウハウ
Go言語入門:よくある質問 -デザイン Vol.9-
この記事は約11分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、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言語のよくある質問について解説しました。

よっしー
よっしー

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

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました