Go言語での並行処理(Concurrency)は、高いパフォーマンスを実現するために非常に重要です。特に、複数の関数やタスクを並行して実行する場合、Goの並行処理の特徴であるゴルーチン(goroutines)とチャネル(channels)を活用することが非常に有効です。この記事では、Go言語で複数の関数を並行して実行する方法を完全かつ包括的に説明します。
1. Go言語における並行処理の基本
Goは、並行処理を扱うために「ゴルーチン(goroutines)」と呼ばれる軽量なスレッドを使用します。ゴルーチンは、Goプログラム内で非同期的に実行される関数やメソッドです。通常、Goではgo
キーワードを使って関数をゴルーチンとして実行します。これにより、関数が非同期に実行され、他のコードと並行して処理が進みます。

2. ゴルーチンの基本的な使い方
ゴルーチンを使って複数の関数を並行して実行する基本的な方法は以下の通りです。
gopackage main
import "fmt"
func task1() {
fmt.Println("タスク1が実行中")
}
func task2() {
fmt.Println("タスク2が実行中")
}
func main() {
go task1() // ゴルーチンでtask1を実行
go task2() // ゴルーチンでtask2を実行
// ゴルーチンが終了する前にmain関数が終了しないように待機
var input string
fmt.Scanln(&input)
}
上記のコードでは、task1
とtask2
の両方を並行して実行しています。go
キーワードを使って、それぞれの関数をゴルーチンとして呼び出しています。fmt.Scanln(&input)
は、ゴルーチンが実行される時間を確保するために使用され、main
関数が終了するのを防ぎます。
3. チャネルを使用したゴルーチンの同期
Goの並行処理では、ゴルーチン間でデータをやり取りするために「チャネル(channels)」を使用します。チャネルを使用することで、ゴルーチン間で安全にデータを送受信でき、複数のゴルーチンの終了を待つことも可能です。
以下は、チャネルを使って複数のゴルーチンが終了するのを待つ例です。
gopackage main
import (
"fmt"
"time"
)
func task1(ch chan string) {
time.Sleep(2 * time.Second)
ch <- "タスク1が完了しました"
}
func task2(ch chan string) {
time.Sleep(1 * time.Second)
ch <- "タスク2が完了しました"
}
func main() {
ch := make(chan string)
go task1(ch)
go task2(ch)
// ゴルーチンの終了を待つ
fmt.Println(<-ch)
fmt.Println(<-ch)
}
このコードでは、task1
とtask2
の実行が終了すると、それぞれのゴルーチンがチャネルch
を通じてメッセージを送ります。main
関数は<-ch
を使ってそのメッセージを受け取り、ゴルーチンが終了するのを待ちます。
4. WaitGroupを使用したゴルーチンの同期
sync.WaitGroup
は、複数のゴルーチンが終了するのを待機するために使います。WaitGroup
を使うと、ゴルーチンの実行が完了するまでmain
関数をブロックし、複数のゴルーチンが終了するのを効率的に待つことができます。
gopackage main
import (
"fmt"
"sync"
"time"
)
func task1(wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(2 * time.Second)
fmt.Println("タスク1が完了しました")
}
func task2(wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(1 * time.Second)
fmt.Println("タスク2が完了しました")
}
func main() {
var wg sync.WaitGroup
wg.Add(2) // 2つのゴルーチンを待機
go task1(&wg)
go task2(&wg)
wg.Wait() // ゴルーチンの終了を待つ
fmt.Println("すべてのタスクが完了しました")
}
このコードでは、sync.WaitGroup
を使って、task1
とtask2
が終了するまでmain
関数が待機します。wg.Add(2)
で2つのゴルーチンの終了を待機し、wg.Done()
でそれぞれのゴルーチンが終了したことを通知します。最後にwg.Wait()
を呼び出して、すべてのゴルーチンの終了を待機します。
5. ゴルーチンとエラーハンドリング
並行処理を行う際には、エラーが発生する可能性もあるため、適切にエラーハンドリングを行うことが重要です。エラーをチャネルで送信し、main
関数で受け取る方法が一般的です。
gopackage main
import (
"fmt"
"errors"
"time"
)
func task1(ch chan<- error) {
time.Sleep(2 * time.Second)
ch <- errors.New("タスク1でエラーが発生しました")
}
func task2(ch chan<- error) {
time.Sleep(1 * time.Second)
ch <- nil // エラーなし
}
func main() {
ch := make(chan error, 2)
go task1(ch)
go task2(ch)
for i := 0; i < 2; i++ {
if err := <-ch; err != nil {
fmt.Println(err)
} else {
fmt.Println("タスクは正常に完了しました")
}
}
}
この例では、task1
とtask2
のそれぞれがエラーをチャネルを通じてmain
関数に送信します。main
関数では、そのエラーを受け取り、エラーハンドリングを行います。
6. 並行処理の最適化
Goの並行処理を最適化するには、タスクの数やゴルーチンの数を適切に管理することが重要です。ゴルーチンは軽量ですが、多くのゴルーチンを起動すると、システムリソースを消費し、パフォーマンスに影響を与える可能性があります。そのため、ゴルーチンの数を制限するためにバッファ付きチャネルやワーカープールのデザインパターンを使用することが推奨されます。
gopackage main
import "fmt"
func worker(id int, ch chan int) {
for task := range ch {
fmt.Printf("ワーカー %d がタスク %d を処理中\n", id, task)
}
}
func main() {
ch := make(chan int, 5) // バッファ付きチャネル
for i := 1; i <= 3; i++ {
go worker(i, ch)
}
for i := 1; i <= 10; i++ {
ch <- i // タスクをチャネルに送信
}
close(ch) // チャネルを閉じてワーカーに終了を通知
}
このコードは、3つのワーカーがそれぞれタスクを処理するというシンプルなワーカープールの例です。タスクはバッファ付きチャネルを通じて送信され、ワーカーは並行してタスクを処理します。
結論
Go言語での並行処理は、効率的に複数の関数やタスクを実行するために非常に強力な手法です。ゴルーチンとチャネルをうまく使うことで、高いパフォーマンスとスケーラビリティを実現できます。ゴルーチンの使用においては、適切な同期手法やエラーハンドリング、そしてリソース管理を考慮することが重要です。