Go言語の魅力の1つが、軽量で扱いやすい並行処理機能です。goroutineとchannelの組み合わせで、シンプルかつ強力な並行プログラムを書けます。本記事では、Goの並行処理の基本を解説します。
goroutineとは
goroutineは「軽量スレッド」で、関数呼び出しの前にgoを付けるだけで並行実行できます。1関数あたり数KBで動くため、数万本の同時実行も現実的です。
func say(msg string) {
fmt.Println(msg)
}
func main() {
go say("hello") // 別goroutineで実行
say("world")
time.Sleep(100 * time.Millisecond)
}
channelとは
channelはgoroutine間で値をやり取りする「型付きパイプ」です。共有メモリではなく「メッセージで通信する」のがGoの基本思想です。
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // 送信
}
close(ch)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch { // 受信
fmt.Println(v)
}
}
バッファ付きchannel
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// 4つ目はブロックする
select文
複数のchannelに対する操作を多重化します。タイムアウトやキャンセル処理にも便利です。
select {
case v := <-ch1:
fmt.Println("ch1:", v)
case v := <-ch2:
fmt.Println("ch2:", v)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
sync.WaitGroupで待ち合わせ
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait()
contextでキャンセル制御
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go func() {
select {
case <-ctx.Done():
fmt.Println("canceled:", ctx.Err())
}
}()
よくある落とし穴
- 共有変数への並行書き込み → mutexまたはchannel経由に
- channelのcloseは送信側が行う
- goroutineリーク(終了条件が無いgoroutine)に注意
- for-loop内のクロージャ変数キャプチャ(Go 1.22以降は改善)
まとめ
Goの並行処理はgoroutine + channel + selectの3点セットで多くの問題を解決できます。共有メモリより「通信で同期する」発想が、Goらしい並行プログラムを書くコツです。慣れれば数行で堅牢な並行処理が書けます。