Go言語の並行処理
2026/07/28

ゴルーチン

ゴルーチンとは、Goランタイムが管理する軽量なスレッドです。関数の前にgoを付けて呼び出すと、その関数の完了を待たずに、すぐ次の文へ進みます。呼び出された関数は、新しいゴルーチンとして元の処理と並行して実行されます。OSスレッドより起動コストが小さく、多数を並行して動かせます。

done := make(chan struct{})
go func() {
    fmt.Println("別のゴルーチンから")
    done <- struct{}{}
}()
fmt.Println("main側")
<-done

main終了とゴルーチンの寿命

mainゴルーチンがreturnするとプログラム全体が終了し、他のゴルーチンも打ち切られます。mainが先に終わると、起動したばかりの処理の完了を待たずに終了します。

go fmt.Println("worker")
// mainがすぐ終わると、workerの出力が欠けることがある

完了を待つには、チャネルやsync.WaitGroupなどの同期が必要です。

送信と受信

チャネルは値を送受信する型です。ゴルーチン間でよく使われます。要素型Tのチャネルはchan Tと書きます。ch <- vはチャネルchへvを送り、<-chはchから値を受け取ります。受信は式にも文にも書けます。x := <-chのように値を受け取るほか、<-chだけで値を捨てることもできます。

ch := make(chan string)
go func() {
    ch <- "ping" // 送信
}()
msg := <-ch      // 受信
fmt.Println(msg) // ping
ch <- v    // 送信
x := <-ch  // 受信
<-ch       // 値を捨てる受信

送受信の両方に使えるchan Tに対し、向きを片方に限った型もあります。chan<- Tは送信専用、<-chan Tは受信専用です。双方向のチャネルは、送信専用や受信専用の変数へ代入できます。合わない向きの操作はコンパイルエラーになります。

ch := make(chan int)
var send chan<- int = ch    // (chan<- int)(ch)でも同じ
var recv <-chan int = ch    // (<-chan int)(ch)でも同じ
go func() { send <- 1 }()
fmt.Println(<-recv)         // OK
// <-send                   // NG: 送信専用から受信できない
// recv <- 1                // NG: 受信専用へ送信できない
// close(recv)              // NG: 受信専用は閉じられない

make(chan int)はバッファなしチャネルです。送信と受信が揃うまで双方がブロックされます。

ch := make(chan int)
go func() {
    ch <- 1    // 受信が来るまでここで待つ
}()
n := <-ch      // 送信が来るまでここで待つ
fmt.Println(n) // 1

ゴルーチンの送信と受信はどちらが先でも問題ありません。

ch := make(chan int)
go func() {
    n := <-ch      // 送信が来るまでここで待つ
    fmt.Println(n) // 1
}()
ch <- 1            // 受信が来るまでここで待つ

バッファなしチャネルは、送る側と受ける側が揃う必要があります。同じゴルーチンだけで送受信すると、相手が現れずその場で止まります。他に動いているゴルーチンがなければ、ランタイムはデッドロックとしてプログラムを終了します。

ch := make(chan int)
ch <- 1 // 受信するゴルーチンがいない → デッドロック

値を渡さず終わったことだけ伝えたいときは、要素型を空の構造体にしたチャネルが使えます。struct{}はフィールドを持たない型で、値そのものに伝える情報はありません。送受信が起きたこと自体が合図になり、chan boolのように使わない中身を載せる必要がありません。サイズも0なので、完了通知だけに向いています。

done := make(chan struct{})
go func() {
    fmt.Println("worker")
    done <- struct{}{} // 完了の合図
}()
<-done                 // mainが待つ → workerの出力を見られる

値の受け渡しが不要で、複数のゴルーチンの終了だけ待つときはsync.WaitGroupが使えます。WaitGroupは内部で完了待ちのタスク数をカウントします。WaitGroup.Goに関数を渡すと、ゴルーチンの起動と完了の追跡をまとめて行います。待ち側でWaitを呼び、すべての処理が終わるまで待機します。

var wg sync.WaitGroup
wg.Go(func() { fmt.Println("worker", 1) })
wg.Go(func() { fmt.Println("worker", 2) })
wg.Wait() // すべてのゴルーチンが終わるまで待つ

手動でAddとDoneを使う方法もあります。Addで待つタスクの数をカウンタに加算し、各ゴルーチンの終了時にDoneで1つ減らします。Addはゴルーチン起動より前に呼びます。起動後にAddすると、カウンタが増える前にWaitが待ち終わったと判断して先に進んでしまうことがあります。

var wg sync.WaitGroup
wg.Add(2)
for id := 1; id <= 2; id++ {
    go func(id int) {
        defer wg.Done()
        fmt.Println("worker", id)
    }(id)
}
wg.Wait()

makeの第2引数が1以上のときバッファ付きチャネルになります。0や省略はバッファなしです。バッファに空きがあれば受信側を待たずに送信でき、値があれば送信側を待たずに受信できます。バッファが満杯のときは送信が、空のときは受信がブロックされます。

ch := make(chan int, 2)
ch <- 10 // 空きがあるのでブロックされない
ch <- 20
fmt.Println(<-ch, <-ch) // 10 20

容量を超えて送ると、空きができるまで送信側が止まります。他に動いているゴルーチンがなければデッドロックになります。

ch := make(chan int, 1)
ch <- 1
// ch <- 2 // 受信が無いとここでブロックされる(他に動くゴルーチンがなければデッドロック)

closeとrange

close(ch)は、これ以上値を送信しないことを伝える操作です。チャネルを閉じるのは基本的に送信側の役割です。受信側が閉じてしまうと、まだ値を送ろうとしている送信側が閉じたチャネルへ送信してしまい、パニックの原因になります。なお、closeは受信専用チャネル(<-chan T)には呼べません。また、すでに閉じたチャネルやnilチャネルを閉じてもパニックになります。

閉じたチャネルへ送ると、実行時にパニックします。

ch := make(chan int)
close(ch)
// ch <- 1 // panic: send on closed channel

通常、空のチャネルから受信しようとすると値が届くまでブロックされますが、閉じたチャネルからは待たされずに値を取り出せます。バッファに残っている値があればそれを取り出し、すべて取り出し終えたあとはゼロ値とfalseが返ります。以降はブロックされず、何度受信してもゼロ値とfalseが即座に返り続けます。

ch := make(chan int, 1)
ch <- 7
close(ch)

v, ok := <-ch
fmt.Println(v, ok) // 7 true(バッファに残っていた値)

v, ok = <-ch
fmt.Println(v, ok) // 0 false(空になってもブロックされず、ゼロ値とfalseが返る)

v, ok = <-ch
fmt.Println(v, ok) // 0 false(何度受信しても同じ)

range chはチャネルが閉じられるまで受信を続けます。送信側がcloseしないと、rangeは終わりません。

ch := make(chan string, 2)
ch <- "a"
ch <- "b"
close(ch)

for s := range ch {
    fmt.Println(s) // a → b
}

select

select文は複数の通信操作のうち、実行可能なcaseを1つ選びます。どれもすぐに実行できないときは、準備できるまで待ちます。複数のcaseが同時に準備できていれば、一様な疑似乱数でそのうち1つが選ばれます。並びの先頭が優先されるわけではありません。

defaultを付けると、どの通信もすぐできないときに待たずそのcaseへ進みます。

ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch1 <- "from ch1"

select {
case msg := <-ch1:
    fmt.Println(msg) // from ch1
case msg := <-ch2:
    fmt.Println(msg)
default:
    fmt.Println("no communication")
}

default無しのselectで、どのcaseも永久に準備できないとそのゴルーチンは止まります。他に動いているゴルーチンがなければデッドロックになります。チャネル操作と同様に、相手側の送受信やcloseがある前提で書きます。

値の可視性

1つのゴルーチン内では、コードに記述された順序どおりに処理が実行されます。しかし複数のゴルーチンが共有メモリを読み書きする場合、同期を行わない限り、あるゴルーチンが行った書き込みがいつ別のゴルーチンから見えるかは保証されません。これは、コンパイラやCPUが実行性能を高めるために、プログラムの意味を変えない範囲で命令の並べ替えを行ったり、レジスタやCPUキャッシュに値を保持したりするためです。

次の例では、書き込み側のゴルーチンがmessageに値を設定したあとにreadyをtrueに更新しています。

var message string
var ready bool

go func() {
    message = "done"
    ready = true
}()

for !ready {
    // readyがtrueになるのを待つ
}
fmt.Println(message) // 空文字列が表示される可能性がある

直感的には、ループを抜けてready == trueが確認できたなら、その前に書かれたmessageも"done"になっていると期待されます。しかし同期が行われていない場合、コンパイラやCPUによる並べ替えによって、読み取り側から見るとready = trueの反映がmessage = "done"より先に見えることがあります。また、readyの更新自体がキャッシュからメモリへ反映されず、ループが終了しない可能性もあります。

happens-before関係

メモリモデルでは、プログラム内のメモリ操作の順序と可視性を規定するためにhappens-beforeという概念を用います。操作Aが操作Bに対してhappens-beforeである、つまり操作Aが操作Bより前のとき、操作Aによるメモリの書き込みは、操作Bから確実に読み取れることが言語仕様として保証されます。単一のゴルーチン内では、プログラムの記述順序に従ってhappens-before関係が成り立ちます。

go func() {
    name := ""
    name = "gopher"
    fmt.Println(name) // 同じゴルーチン内なので必ずgopher
}()

go文によるゴルーチンの起動は、そのゴルーチンの開始に対してhappens-beforeとなります。そのため、go文より前に行われた書き込みは、起動されたゴルーチンから確実に読み取ることができます。

name := "gopher"
go func() {
    fmt.Println(name) // 必ずgopher
}()

一方で、ゴルーチンの終了自体は他のいかなる操作に対してもhappens-before関係を持ちません。そのため、起動されたゴルーチン内で行われた書き込みは、チャネルやミューテックスなどで明示的に同期しない限り、起動元のゴルーチンから読み取れる保証はありません。

var name string
go func() {
    name = "gopher"
}()
fmt.Println(name) // 空文字列のことがある

このようにチャネルの送受信で同期を明示的に行うと、送信前の書き込みが受信後に必ず読み取れます。

var name string
done := make(chan struct{})
go func() {
    name = "gopher"
    done <- struct{}{}
}()
<-done
fmt.Println(name) // 必ずgopher

closeは、送信と同じ向きの順序付けができます。閉じるより前の書き込みは、閉じたチャネルからの受信後に必ず読み取れます。

var name string
done := make(chan struct{})
go func() {
    name = "gopher"
    close(done)
}()
<-done
fmt.Println(name) // 必ずgopher

バッファなしチャネルmake(chan T)では、送信側と受信側が揃って初めて通信が成立します。そのため送信の完了が受信の完了に対してhappens-beforeとなるだけでなく、受信の完了も送信の完了に対してhappens-beforeとなります。

一方、バッファ付きチャネルmake(chan T, n)では、バッファに空きがある限り、受信側を待たずに送信が即座に完了します。容量がnのバッファ付きチャネルで仕様上保証されるのは、k回目の受信完了は、k+n回目の送信完了に対してhappens-beforeとなるという規則です。

var name string
done := make(chan struct{}, 1)
go func() {
    name = "gopher"
    <-done
}()
done <- struct{}{}
fmt.Println(name) // 空文字列のことがある

nが1のとき、1回目の受信は2回目の送信の完了に対してhappens-beforeになります。2回目の送信はバッファが空くまで待つので、その完了後には受信より前の書き込みが見えます。

var name string
done := make(chan struct{}, 1)
go func() {
    name = "gopher"
    <-done
}()
done <- struct{}{}
done <- struct{}{}
fmt.Println(name) // 必ずgopher

空きバッファへの送信は相手の受信を待たずに進むため、送信完了だけでは受信側の書き込みを読み取れる保証にはなりません。一方的な通知ではなく双方向の同期が必要な場合は、バッファなしチャネルを使うか、完了通知用のチャネルを別途設けます。

データ競合

2つ以上のゴルーチンがメモリ上の同じ位置へ同時にアクセスし、少なくとも1つが書き込みであり、かつそれらのアクセス間にhappens-before関係がない状態をデータ競合と呼びます。データ競合が起きると、意図した値が読めなくなるだけでなく、内部で複数の情報(ポインタや長さなど)を管理するstringやスライスでは値の破損やクラッシュにつながります。bool1つの代入であっても、同期のない並行アクセスはデータ競合になります。

var counter int

go func() {
    counter++
}()

fmt.Println(counter) // データ競合: 0か1かは決まらない

Go言語にはデータ競合を検出するレースディテクタが標準で備わっています。実行時やテスト時に-raceフラグを付与します。

go test -race ./...
go run -race main.go

レースディテクタは実行時のメモリアクセスを監視して競合を報告します。テストで実行されなかったコードパスの競合は検出できないため、並行処理を行うすべての経路をテストで動かす必要があります。

sync.Mutexによる排他制御とhappens-before

複数のゴルーチンが共有メモリを読み書きする場合、sync.Mutexによるロックを使ってアクセスを排他制御します。sync.Mutexは同時に1つのゴルーチンしか保護区間に入れないようにするだけでなく、メモリの可視性に関するhappens-before関係も保証します。

あるゴルーチンによるUnlockの呼び出し完了は、その後に成功する任意のゴルーチンによるLockの呼び出しに対してhappens-beforeとなります。したがって、あるゴルーチンがUnlockを呼ぶ前に行ったすべてのメモリ書き込みは、次にLockの取得に成功したゴルーチンから確実に読み取れます。

var (
    mu sync.Mutex
    n  int
)

mu.Lock()
go func() {
    defer mu.Unlock()
    n++
}()
mu.Lock()
fmt.Println(n) // 必ず1
mu.Unlock()