🔥

Go初心者がゴルーチンについて調べてみた

2024/12/22に公開

はじめに

ちょうど 1 年前に現職へ転職し、現在の会社で Go 言語を使い始めました。ゴルーチンについては何となく知っていたものの、業務で使う機会がなく、ふんわりとした知識のままでした。そこで、せっかくの機会なので ゴルーチンについて調べてまとめてみました。

ゴルーチンとは

ゴルーチンは、Go 言語における並列処理を簡単に実現するための仕組みです。並列処理とは、複数の処理を同時に進行させることです。これにより、効率的なリソースの利用や高速な処理が可能になります。

使い方

実際にコードを書いて確認してみましょう。
基本的な文法は以下になります。

func main() {
  // go 関数名() でゴルーチンを作成
  go func() {
    fmt.Println("Hello from a goroutine!")
  }()
  fmt.Println("Hello from the main function")
  time.Sleep(time.Second)
}
# 実行結果
Hello from the main function
Hello from a goroutine!

先に記述されている「Hello from a goroutine!」を出力する処理が、後に記述されている「Hello from the main function」を出力する処理よりも先に実行されていることがわかります。

ゴルーチンは非同期で実行されるため、メイン関数の処理が終わる前にゴルーチンの処理が実行されることがあります。

今度は最後に記述されている、time.Sleep(time.Second) をコメントアウトしてみましょう。

func main() {
  go func() {
    fmt.Println("Hello from a goroutine!")
  }()
  fmt.Println("Hello from the main function")
  // time.Sleep(time.Second)
}
# 実行結果
Hello from the main function

今度は「Hello from the main function」だけが出力されて「Hello from a goroutine!」が出力されません。これは、メイン関数が終了するとゴルーチンも終了してしまうためです。

では、ゴルーチンが終わるまで待つ方法はあるのでしょうか?もちろんそんなことはありません。
sync.WaitGroup を使うことで、ゴルーチンの処理が終わるまで待つことができます。

func main() {
  var wg sync.WaitGroup // sync.WaitGroup を作成
  wg.Add(1) // 待機するゴルーチンの数を追加
  go func() {
    fmt.Println("Hello from a goroutine!")
    wg.Done() // カウンターをデクリメント
  }()
  fmt.Println("Hello from the main function")
  wg.Wait() // カウンターが 0 になるまで待機
}
# 実行結果
Hello from the main function
Hello from a goroutine!

これで sleep を使わずにゴルーチンの処理が終わるまで待つことができました。

チャネルと ゴルーチン

チャネルについても触れておきます。
チャネルとは、ゴルーチン間でデータをやり取りするための仕組みです。チャネルを使うことで、ゴルーチン間でデータを安全にやり取りすることができます。

まずはチャネルの基本的な文法について確認していきましょう。

ch := make(chan int) // int型のデータをやり取りするチャネルを作成
ch := make(chan int, 5) // バッファサイズを指定してチャネルを作成

データの送受信は以下のように実行します。

ch <- 1 // チャネルにデータを送信
i := <-ch // チャネルからデータを受信

fmt.Println(i) // 1を出力

以下は、チャネルを使ってゴルーチン間でデータをやり取りする例です。

func main() {
  ch := make(chan int)
  go func() {
    ch <- 1
  }()
  i := <-ch
  fmt.Println(i)
}
# 実行結果
1

このようにしてチャネルを利用してデータをやり取りすることができます。

しかし、このままだとチャネルがクローズされずにチャネルが待機状態になってしまいます。そのため、チャネルをクローズする必要があります。
以下のように close(ch) を使ってチャネルをクローズします。

func main() {
  ch := make(chan int)
  go func() {
    ch <- 1
    close(ch)
  }()
  for i := range ch {
    fmt.Println(i)
  }
}
# 実行結果
1

これでチャンネルが待機した状態を解除することができます。

最後に、チャネルを使って複数のゴルーチン間でデータをやり取りする例としてサンプルコードを記載しておきます。

type Task struct {
	Number int
}

type Result struct {
	Number int
	Square int
}

// 平方を計算する関数
func calculateSquare(task Task, results chan<- Result, wg *sync.WaitGroup) {
	defer wg.Done() // ゴルーチンの終了を通知

	fmt.Printf("Calculating square for number: %d\n", task.Number)
	time.Sleep(500 * time.Millisecond) // 計算をシュミレート
	square := task.Number * task.Number

	results <- Result{task.Number, square} // 結果をチャネルに送信

	fmt.Printf("Calculated square for number: %d, result: %d\n", task.Number, square)
}

func main() {
	var wg sync.WaitGroup
	tasks := []Task{{1}, {2}, {3}, {4}, {5}}
	result := make(chan Result, len(tasks)) // チャネルを作成

	for _, task := range tasks {
		wg.Add(1)                             // カウンターを増やす
		go calculateSquare(task, result, &wg) // ゴルーチンで関数を実行
	}

	wg.Wait()     // すべてのゴルーチンが終了するまで待機
	close(result) // チャネルを閉じる

	// 結果を出力
	fmt.Println("\nResults:")
	for res := range result {
		fmt.Printf("Square of %d is: %d\n", res.Number, res.Square)
	}
}
# 実行結果
Calculating square for number: 5
Calculating square for number: 1
Calculating square for number: 2
Calculating square for number: 3
Calculating square for number: 4
Calculated square for number: 3, result: 9
Calculated square for number: 4, result: 16
Calculated square for number: 5, result: 25
Calculated square for number: 2, result: 4
Calculated square for number: 1, result: 1

Results:
Square of 1 is: 1
Square of 3 is: 9
Square of 4 is: 16
Square of 5 is: 25
Square of 2 is: 4

ゴルーチンのいいと思った所

実際に試してみて、ゴルーチンのいいと思った点をいくつか挙げてみます。

簡単に記述できる

まずは、ゴルーチンを使うことで、簡単に並列処理を実現できるという点です。
先ほどの例でもわかる通り、go 関数名() と一行追加するだけでゴルーチンを作成することができます。

例えば PHP で並列処理を行う場合、プロセスの作成や管理など、より複雑な実装が必要になります。外部コマンドの実行やプロセス間通信の処理を明示的に書く必要があり、コードも長くなりがちです。

高いスケーラビリティ

ゴルーチンは軽量であり、いくつもの同時に実行することが可能だそうです。Go ランタイムがゴルーチンのスケジューリングを効率的に行うためです。例えば、以下のようにいくつものゴルーチンを作成してみましょう。

func main() {
  var wg sync.WaitGroup
  for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func(i int) {
      defer wg.Done()
      fmt.Printf("Goroutine %d\n", i)
    }(i)
  }
  wg.Wait()
}
# 実行結果(並列処理のため順番は保証されない)
Goroutine 21
Goroutine 11
Goroutine 12
...
Goroutine 908
Goroutine 79
Goroutine 953

このように、多数のゴルーチンを簡単に作成し、並列処理を行うことができます。

まとめ

今回ゴルーチンについて調べてみたので、その内容を自分のメモがてらまとめてみました。
実際に書いてみると、簡単に並列処理を書くことができて便利だなと感じました。

やっぱり調べた内容を自分なりにまとめることは大事だなと思ったので、これからも続けていきたい。(ほぼポエムなので足りない部分はご容赦ください)

Discussion