Go言語入門:効果的なGo -Goroutines-

スポンサーリンク
Go言語入門:効果的なGo -Goroutines- 用語解説
Go言語入門:効果的なGo -Goroutines-
この記事は約17分で読めます。
よっしー
よっしー

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

本日は、Go言語を効果的に使うためのガイドラインについて解説しています。

スポンサーリンク

背景

Go言語を学び始めて、より良いコードを書きたいと思い、Go言語の公式ドキュメント「Effective Go」を知りました。これは、いわば「Goらしいコードの書き方指南書」になります。単に動くコードではなく、効率的で保守性の高いコードを書くためのベストプラクティスが詰まっているので、これを読んだ時の内容を備忘として残しました。

ゴルーチン

これらはゴルーチンと呼ばれています。なぜなら、既存の用語—スレッド、コルーチン、プロセスなど—は不正確な含意を伝えるからです。ゴルーチンには単純なモデルがあります:それは、同じアドレス空間内で他のゴルーチンと並行して実行される関数です。それは軽量で、スタック空間の割り当て以上のコストはほとんどかかりません。そして、スタックは小さく始まるため、安価であり、必要に応じてヒープストレージを割り当て(そして解放し)ることで成長します。

ゴルーチンは複数のOSスレッドに多重化されるため、1つがI/Oを待つなどでブロックしても、他は実行を続けます。その設計は、スレッドの作成と管理の複雑さの多くを隠します。

関数またはメソッドの呼び出しにgoキーワードを前置すると、新しいゴルーチンでその呼び出しが実行されます。呼び出しが完了すると、ゴルーチンは静かに終了します。(効果は、コマンドをバックグラウンドで実行するためのUnixシェルの&記法に似ています。)

go list.Sort()  // list.Sortを並行実行;完了を待たない。

関数リテラルはゴルーチンの呼び出しで便利です。

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // 括弧に注意 - 関数を呼び出す必要があります。
}

Goでは、関数リテラルはクロージャです:実装は、関数によって参照される変数が、それらがアクティブである限り生き残ることを確実にします。

これらの例は、関数が完了を通知する方法がないため、あまり実用的ではありません。そのためには、チャンネルが必要です。

ゴルーチンとは?

ゴルーチンは、Goにおける軽量な並行実行の仕組みです。従来のスレッドよりもはるかに軽量で、簡単に使用できます。

基本的な使用法

package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Printf("Hello, %s! (%d)\n", name, i+1)
        time.Sleep(500 * time.Millisecond)
    }
}

func main() {
    fmt.Println("=== 基本的なゴルーチンの例 ===")
    
    // 通常の関数呼び出し(同期実行)
    fmt.Println("同期実行:")
    sayHello("Alice")
    
    fmt.Println("\n並行実行:")
    // ゴルーチンでの実行(非同期実行)
    go sayHello("Bob")
    go sayHello("Charlie")
    
    // メインゴルーチンが終了する前に少し待つ
    time.Sleep(2 * time.Second)
    fmt.Println("メイン終了")
}

関数リテラル(無名関数)との組み合わせ

package main

import (
    "fmt"
    "time"
)

func Announce(message string, delay time.Duration) {
    // 無名関数をゴルーチンで実行
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }() // () で関数を実際に呼び出し
}

func main() {
    fmt.Println("=== 関数リテラルの例 ===")
    
    // 複数のアナウンスを並行実行
    Announce("メッセージ1(1秒後)", 1*time.Second)
    Announce("メッセージ2(2秒後)", 2*time.Second)
    Announce("メッセージ3(500ms後)", 500*time.Millisecond)
    
    // 全てのアナウンスが表示されるまで待機
    time.Sleep(3 * time.Second)
    fmt.Println("全アナウンス完了")
}

クロージャの特性

package main

import (
    "fmt"
    "time"
)

func closureExample() {
    fmt.Println("=== クロージャの例 ===")
    
    message := "外部変数"
    counter := 0
    
    // クロージャ:外部の変数にアクセス可能
    for i := 0; i < 3; i++ {
        go func(id int) {
            counter++ // 外部変数にアクセス
            fmt.Printf("ゴルーチン %d: %s, カウンター=%d\n", id, message, counter)
        }(i)
        
        time.Sleep(100 * time.Millisecond)
    }
    
    time.Sleep(1 * time.Second)
    fmt.Printf("最終カウンター: %d\n", counter)
}

func main() {
    closureExample()
}

実用的な例:並行ダウンロード

package main

import (
    "fmt"
    "io"
    "net/http"
    "time"
)

func fetchURL(url string, result chan<- string) {
    start := time.Now()
    
    resp, err := http.Get(url)
    if err != nil {
        result <- fmt.Sprintf("エラー %s: %v", url, err)
        return
    }
    defer resp.Body.Close()
    
    // レスポンスサイズを取得
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        result <- fmt.Sprintf("読み込みエラー %s: %v", url, err)
        return
    }
    
    duration := time.Since(start)
    result <- fmt.Sprintf("完了 %s: %d bytes, %v", url, len(body), duration)
}

func concurrentDownloadExample() {
    fmt.Println("=== 並行ダウンロードの例 ===")
    
    urls := []string{
        "https://httpbin.org/delay/1",
        "https://httpbin.org/delay/2",
        "https://httpbin.org/json",
        "https://httpbin.org/status/200",
    }
    
    results := make(chan string, len(urls))
    
    // 全URLを並行でダウンロード
    for _, url := range urls {
        go fetchURL(url, results)
    }
    
    // 結果を受信
    for i := 0; i < len(urls); i++ {
        fmt.Println(<-results)
    }
}

func main() {
    concurrentDownloadExample()
}

ワーカープールパターン

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

type Task struct {
    ID   int
    Data string
}

type Result struct {
    TaskID int
    Output string
    Worker int
}

func worker(id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()
    
    for task := range tasks {
        // タスク処理をシミュレート
        processingTime := time.Duration(rand.Intn(1000)) * time.Millisecond
        time.Sleep(processingTime)
        
        result := Result{
            TaskID: task.ID,
            Output: fmt.Sprintf("処理済み: %s", task.Data),
            Worker: id,
        }
        
        results <- result
        fmt.Printf("ワーカー %d: タスク %d 完了\n", id, task.ID)
    }
}

func workerPoolExample() {
    fmt.Println("=== ワーカープールの例 ===")
    
    const numWorkers = 3
    const numTasks = 10
    
    tasks := make(chan Task, numTasks)
    results := make(chan Result, numTasks)
    
    var wg sync.WaitGroup
    
    // ワーカーを開始
    for i := 1; i <= numWorkers; i++ {
        wg.Add(1)
        go worker(i, tasks, results, &wg)
    }
    
    // タスクを送信
    go func() {
        for i := 1; i <= numTasks; i++ {
            tasks <- Task{
                ID:   i,
                Data: fmt.Sprintf("データ-%d", i),
            }
        }
        close(tasks)
    }()
    
    // 結果収集
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // 結果を表示
    for result := range results {
        fmt.Printf("結果: %+v\n", result)
    }
    
    fmt.Println("全タスク完了")
}

func main() {
    workerPoolExample()
}

生産者・消費者パターン

package main

import (
    "fmt"
    "sync"
    "time"
)

func producer(name string, data chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    defer close(data)
    
    for i := 1; i <= 5; i++ {
        value := i * 10
        data <- value
        fmt.Printf("Producer %s: %d を生産\n", name, value)
        time.Sleep(200 * time.Millisecond)
    }
    
    fmt.Printf("Producer %s 完了\n", name)
}

func consumer(name string, data <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    
    for value := range data {
        // データ処理をシミュレート
        time.Sleep(300 * time.Millisecond)
        fmt.Printf("Consumer %s: %d を消費\n", name, value)
    }
    
    fmt.Printf("Consumer %s 完了\n", name)
}

func producerConsumerExample() {
    fmt.Println("=== 生産者・消費者の例 ===")
    
    data := make(chan int, 3) // バッファ付きチャンネル
    
    var producerWg sync.WaitGroup
    var consumerWg sync.WaitGroup
    
    // 生産者を開始
    producerWg.Add(1)
    go producer("P1", data, &producerWg)
    
    // 消費者を開始
    consumerWg.Add(2)
    go consumer("C1", data, &consumerWg)
    go consumer("C2", data, &consumerWg)
    
    // 生産完了を待ってから消費完了を待つ
    producerWg.Wait()
    consumerWg.Wait()
    
    fmt.Println("全体完了")
}

func main() {
    producerConsumerExample()
}

タイムアウト処理

package main

import (
    "fmt"
    "time"
)

func longRunningTask(id int, duration time.Duration, result chan<- string) {
    time.Sleep(duration)
    result <- fmt.Sprintf("タスク %d 完了 (%v)", id, duration)
}

func timeoutExample() {
    fmt.Println("=== タイムアウト処理の例 ===")
    
    result := make(chan string, 1)
    timeout := 2 * time.Second
    
    // 長時間実行されるタスクを開始
    go longRunningTask(1, 3*time.Second, result)
    
    select {
    case res := <-result:
        fmt.Println("結果:", res)
    case <-time.After(timeout):
        fmt.Printf("タイムアウト: %v 以内に完了しませんでした\n", timeout)
    }
    
    // 短時間で完了するタスク
    go longRunningTask(2, 1*time.Second, result)
    
    select {
    case res := <-result:
        fmt.Println("結果:", res)
    case <-time.After(timeout):
        fmt.Printf("タイムアウト: %v 以内に完了しませんでした\n", timeout)
    }
}

func main() {
    timeoutExample()
}

実際の問題を解決する例:ファイル並行処理

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

type FileInfo struct {
    Name string
    Size int
}

func processFile(file FileInfo, results chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    
    // ファイル処理をシミュレート(サイズに応じて処理時間が変わる)
    processingTime := time.Duration(file.Size/100) * time.Millisecond
    time.Sleep(processingTime)
    
    results <- fmt.Sprintf("処理完了: %s (%d bytes, %v)", 
        file.Name, file.Size, processingTime)
}

func fileProcessingExample() {
    fmt.Println("=== ファイル並行処理の例 ===")
    
    // テスト用のファイルリスト
    files := []FileInfo{
        {"document1.txt", rand.Intn(1000) + 100},
        {"image1.jpg", rand.Intn(2000) + 500},
        {"video1.mp4", rand.Intn(5000) + 1000},
        {"document2.txt", rand.Intn(1000) + 100},
        {"image2.jpg", rand.Intn(2000) + 500},
    }
    
    results := make(chan string, len(files))
    var wg sync.WaitGroup
    
    start := time.Now()
    
    // 全ファイルを並行処理
    for _, file := range files {
        wg.Add(1)
        go processFile(file, results, &wg)
    }
    
    // 完了待ちとチャンネルクローズ
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // 結果を受信
    for result := range results {
        fmt.Println(result)
    }
    
    totalTime := time.Since(start)
    fmt.Printf("全ファイル処理完了: %v\n", totalTime)
}

func main() {
    fileProcessingExample()
}

重要なポイント

1. ゴルーチンの特徴

  • 軽量: スレッドよりもはるかに少ないメモリ使用量
  • 簡単: go キーワードを付けるだけで並行実行
  • 効率的: OSスレッドに多重化されて実行

2. 基本的な使用法

go functionName()          // 関数を並行実行
go func() { ... }()        // 無名関数を並行実行

3. メモリ管理

  • 小さなスタック: 最初は2KB程度で開始
  • 動的拡張: 必要に応じて自動的に拡張
  • ガベージコレクション: 不要になったら自動回収

4. クロージャの活用

  • 外部変数にアクセス可能
  • 変数の寿命がゴルーチンと連動
  • 関数リテラルで柔軟な処理が可能

5. 注意点

  • メインゴルーチン終了: メインが終わると全ゴルーチンも終了
  • 完了通知: チャンネルやWaitGroupで同期が必要
  • 競合状態: 共有変数アクセスには注意

6. 実用的なパターン

  • ワーカープール: 限られた数のワーカーでタスク処理
  • 生産者・消費者: データの生成と処理を分離
  • タイムアウト: 長時間処理の制限
  • 並行ダウンロード: ネットワーク処理の並行化

ゴルーチンは、Goの並行性を支える核心技術です。従来のスレッドプログラミングの複雑さを大幅に軽減し、直感的で効率的な並行プログラムを書くことができます。

おわりに 

本日は、Go言語を効果的に使うためのガイドラインについて解説しました。

よっしー
よっしー

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

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

コメント

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