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

ゴルーチンとOSスレッド

goを付けた呼び出しは、元の処理と並行して走ります。ゴルーチンはGoランタイムが管理する実行単位です。OSスレッドより起動コストが小さく、多数を同時に動かせます。ランタイムは複数のゴルーチンを少数のOSスレッドに割り当てて実行します。

構文はgoのあとに関数呼び出しを書きます。呼び出し先の処理が終わるのを待たず、go文の次の文がすぐ実行されます。

go func() {
    fmt.Println("別のゴルーチンから")
}()
fmt.Println("main側")

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

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

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

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

送信と受信

チャネルはゴルーチン間で値を渡す型です。要素型Tのチャネルはchan Tと書きます。

ch <- vはチャネルchvを送り、<-chchから値を受け取ります。<-の左側がチャネルです。

受信は式にも文にも書けます。x := <-chのように値を受け取るほか、<-chだけで値を捨てることもできます。

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

バッファなしチャネルの待ち合わせ

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が使えます。Addで待つ数を登録し、各ゴルーチンの終了時にDone、待ち側で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() // 両方のDoneが終わるまで待つ

Addはゴルーチン起動より前に呼びます。起動後にAddすると、カウンタがまだ増える前にWaitが「待ちは終わった」と判断して先に進んでしまうことがあります。

バッファ付きチャネル

make(chan int, 3)は容量3のバッファ付きチャネルです。バッファに空きがある間は、受信側を待たずに送信できます。バッファが満杯なら送信はブロックし、空なら受信はブロックします。

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)は「これ以上値を送らない」というシグナルです。送信側が呼ぶのが慣習です。受信側が閉じると役割が壊れ、そのあと送信側が送り続けると閉じたチャネルへの送信でパニックになります。

閉じたチャネルへの送信

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

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

閉じたチャネルからの受信

残りの値を取り出したあと、ゼロ値と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(閉じ済み)

rangeとclose

range chはチャネルが閉じられるまで受信を続けます。送信側がcloseしないと、rangeは終わりません。複数の値を送り終えたあと、完了の合図としてもcloseが使えます。

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がある前提で書きます。

他のゴルーチンから見えるとは

Goのメモリモデルは、他のゴルーチンの書き込みが、いつ自分の読み取りから見えるかを定めます。次の例では、別ゴルーチンがreadyに書いたあとでも、読み取り側でtrueとは限りません。

var ready bool
go func() {
    ready = true
}()
_ = ready // readyがtrueとは限らない

ソースに書いた文の順どおりに、他のゴルーチンからも見えるとは限りません。コンパイラやCPUは速度のために、1つのゴルーチン内の読み書きの実行順を入れ替えることがあり(並べ替え)、書き込みの一部だけが先に観測されることがあります。

次の例では、書き込み側はaのあとにbを更新しています。それでも読み取り側が先にb == 2を見ても、その時点でa == 1とは限りません。2のあとに0と表示されることがあります。

var a, b int

go func() {
    a = 1
    b = 2
}()

fmt.Print(b)
fmt.Print(a) // 「20」や「00」などになりうる。「21」とは限らない

書き込みを確実に見せるには、チャネルの送受信やsync.WaitGroupで相手の完了を待つか、sync.Mutexで同じ変数へのアクセスを排他します。こうした、ゴルーチン同士の順序や排他をランタイムが保証する操作を同期と呼びます。

データ競合

2つ以上のゴルーチンが、同期を挟まずに同じ変数へアクセスし、少なくとも1つが書き込みであるとき、データ競合が起きます。競合するアクセスの結果は保証されません。boolへの1回の代入でも、他ゴルーチンからの可視性は保証されません。

var n int
go func() { n = 1 }()
fmt.Println(n) // 0とも1とも限らない(データ競合)

go test -raceで検出できます。検出器は実行したパスに依存するため、通らなかった経路の競合は残る可能性があります。

チャネルで見えるようにする

バッファの有無を問わず、チャネルへの送信は、対応する受信の完了より先に起きたとみなされます。送信より前の書き込みは、受信が終わったあとの読み取りから見えます。

var ready bool
ch := make(chan struct{})

go func() {
    ready = true
    ch <- struct{}{}
}()

<-ch
// この時点でready == trueが見える

値を送る代わりにclose(ch)しても同じです。閉じる前に書いた内容は、相手が受信で「閉じた」と分かったあとに見えます。

var ready bool
ch := make(chan struct{})

go func() {
    ready = true
    close(ch) // 送信の代わりに閉じる
}()

<-ch // 閉じたチャネルからの受信
// この時点でもready == trueが見える

チャネルを使わずreadyだけを読むと、上の保証は付きません。

バッファ付きチャネルでは、送信が受信を待たずに進むことがあります。その場合でも、「対応する受信が完了したあと」であれば、その送信より前の書き込みは受信側から見えます。

逆に、受信する側が書いた値を、送信が終わっただけで読もうとすると、バッファ付きでは見え方が保証されません。次の例では別ゴルーチンが先に受信し、mainが送信しています。バッファに空きがあるためch <- struct{}{}は相手の受信を待たずに進められ、その直後のreadyの読み取りでtrueとは限りません。

var ready bool
ch := make(chan struct{}, 1) // バッファ付き

go func() {
    ready = true
    <-ch // 受信側の書き込みのあとで受信
}()

ch <- struct{}{}
_ = ready // readyがtrueとは限らない(バッファ付きのため)

バッファなし(make(chan struct{}))なら、受信が先に待っていても送信が後から来ても、送受信は対になって同期します。受信が完了した時点で、対応する送信より前の書き込み(ここではready = true)が見えます。

Mutexで同じ変数を守る

複数のゴルーチンが同じ変数を同時に読み書きすると、データ競合になります。同時に触らせないようにする道具がsync.Mutexです。

Mutexは通行証1枚だと考えます。Lockに成功したゴルーチンだけが通行証を持ち、そのあいだに共有変数を更新します。他のゴルーチンが同じMutexLockすると、前のゴルーチンがUnlockするまで待ちます。

var (
    mu sync.Mutex
    n  int
)

mu.Lock()
n++
mu.Unlock()

守りたい変数へ触るすべての経路で、同じmuを先にLockします。Lockせずにnを触るゴルーチンがいれば、そのアクセスは待ち行列に入らず、排他になりません。muは「どの変数を守るか」を覚えておらず、同じ通行証を使う処理どうしが順番待ちになる、という仕組みです。

データとロックをまとめて扱うときは、構造体のフィールドに置くことも多いです。置き場所が変わっても手順は同じで、t.nに触る前にt.mu.Lockします。

type Tally struct {
    mu sync.Mutex
    n  int
}

func (t *Tally) Inc() {
    t.mu.Lock()
    t.n++
    t.mu.Unlock()
}

func (t *Tally) Value() int {
    t.mu.Lock()
    defer t.mu.Unlock()
    return t.n
}

あるゴルーチンのUnlockのあと、別ゴルーチンが同じミューテックスでLockに成功した時点から、そのUnlockより前の書き込みが見えます。Incで増やしたnは、後続のValueから読めます。

LockUnlockのペアを崩すと、排他も可視性の保証も壊れます。Unlockを忘れないよう、関数を抜ける経路が複数あるときはdefer t.mu.Unlock()が使われます。