Go言語は並行処理を非常に簡単に扱える言語として知られています。その核となるのがgoroutineとchannelです。本記事では、これらの基本的な使い方から実践的な例まで、わかりやすく解説します。Go言語で効率的で高速な並行プログラムを書きたい方は、ぜひご一読ください。
goroutineとは何か
goroutineは、Go言語が提供する軽量なスレッドのようなものです。通常のスレッドと比較すると、メモリ使用量が少なく、起動が非常に高速です。Go言語のランタイムは複数のgoroutineを効率的にスケジュールするため、数千から数百万のgoroutineでも扱うことができます。
- メモリ効率が高い(スレッドの1000倍以上軽量)
- 起動と管理が簡単
- ランタイムが自動的にスケジューリング
- 複数のCPUコアを活用可能
goroutineの基本的な使い方
goroutineはgoキーワードを関数の前に付けるだけで起動できます。以下の例を見てみましょう。
package main
import (
"fmt"
"time"
)
func printMessage(msg string) {
for i := 0; i < 5; i++ {
fmt.Println(msg, i)
time.Sleep(time.Second)
}
}
func main() {
go printMessage("Goroutine 1")
go printMessage("Goroutine 2")
// メインのgoroutineが終了しないように待機
time.Sleep(6 * time.Second)
}
このコードは2つのgoroutineを並行して実行します。goキーワードを付けた関数呼び出しは、バックグラウンドで実行され、メインプログラムはすぐに次の行へ進みます。
channelについて理解する
channelは、goroutine間でデータを安全に受け渡すための仕組みです。channelを使うことで、複数のgoroutine間での同期と通信が簡潔に実現できます。
「Do not communicate by sharing memory; instead, share memory by communicating.」(メモリを共有することで通信するのではなく、通信することでメモリを共有しなさい)- Go言語の設計思想
channelの基本的な使い方
channelはmake関数で作成し、<-演算子でデータを送受信します。
package main
import "fmt"
func main() {
// int型のchannelを作成
ch := make(chan int)
// goroutineでchannelにデータを送信
go func() {
ch <- 42 // 42をchannelに送信
}()
// channelからデータを受信
value := <-ch
fmt.Println("受信した値:", value)
}
このコードでは、goroutine内で値をchannelに送信し、メインgoroutineで受信しています。channelは送受信が完了するまでブロックするため、自動的に同期が取られます。
バッファ付きchannelの活用
デフォルトのchannelはバッファがない(バッファサイズが0)ため、送受信が同時に起こらないとデッドロックになります。バッファを持たせることで、複数の値を蓄積できます。
package main
import "fmt"
func main() {
// バッファサイズ3のchannelを作成
ch := make(chan string, 3)
// 3つの値を送信(ブロックしない)
ch <- "apple"
ch <- "banana"
ch <- "cherry"
// 3つの値を受信
fmt.Println(<-ch) // apple
fmt.Println(<-ch) // banana
fmt.Println(<-ch) // cherry
}
バッファ付きchannelは、送信元と受信元のタイミングがずれている場合に便利です。
複数のgoroutineと並行処理の実例
より実践的な例として、複数のworker goroutineがタスクを処理する場面を考えてみます。
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d が job %d を処理中\n", id, job)
time.Sleep(time.Second)
results <- job * 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
// 3つのworkerを起動
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 5つのjobを投入
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // jobsのchannelを閉じる
// 結果を受け取る
for r := 1; r <= 5; r++ {
fmt.Println("結果:", <-results)
}
}
このコードでは、3つのworkerが同時に複数のタスクを処理しています。close()関数でchannelを閉じることで、rangeループを適切に終了させることができます。
select文による複数channelの制御
複数のchannelを同時に監視する必要がある場合、select文を使用します。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "メッセージ1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "メッセージ2"
}()
// 2つのchannelのうち、先に値が来た方を受信
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("受信:", msg1)
case msg2 := <-ch2:
fmt.Println("受信:", msg2)
}
}
}
select文は複数のchannelの受信を監視し、いずれかが準備できた時点で実行します。タイムアウト処理やデフォルトケースも組み合わせて使用できます。
channelのクローズとrange
channelをclose()で閉じると、受信側で値がなくなったことを検出できます。
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch) // channelをクローズ
}()
// rangeでchannelから値を受け取る
for value := range ch {
fmt.Println(value)
}
fmt.Println("完了")
}
rangeループはchannelが閉じられると自動的に終了します。これはタスク完了の通知を待つ場合に非常に便利です。
注意点とベストプラクティス
- デッドロックに注意:バッファなしchannelで送信側と受信側が揃わないと、プログラムがハング
- クローズ済みchannelへの送信はパニック:必ず送信側でchannelをクローズしましょう
- 受信側でのクローズは避ける:送信元が複数の場合、クローズがエラーになる
- goroutineの生存期間管理:メインgoroutineが終了する前に全goroutineが完了することを確認
syncパッケージの活用:WaitGroupを使ってgoroutineの完了を待つと安全
WaitGroupを使った安全な並行処理
複数のgoroutineの完了を確実に待つには、sync.WaitGroupを使うのが推奨です。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
results := make(chan int)
// 3つのgoroutineを登録
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done() // goroutine完了時に呼ばれる
results <- id * 10
}(i)
}
// 結果を受け取りながら並行実行
go func() {
wg.Wait() // 全goroutineの完了を待つ
close(results)
}()
for result := range results {
fmt.Println("結果:", result)
}
}
WaitGroupを使うことで、全goroutineの完了を確実に確認でき、プログラムが途中で終了するリスクを回避できます。
まとめ
- goroutineは
goキーワード一つで簡単に並行処理を実現できる軽量スレッド - channelはgoroutine間での安全なデータ受け渡しを可能にする通信機構
- バッファなしchannelは送受信が同期し、バッファ付きchannelは複数値を蓄積可能
select文で複数channelの監視や、タイムアウト処理が実現できるsync.WaitGroupを使うことで、全goroutineの完了を安全に待つことができる- channelのクローズやデッドロック対策など、エラーハンドリングに注意が必要
- Go言語の並行処理は設計思想に基づき、メモリ共有ではなく通信でデータを共有する

