Goのgoroutine入門:並行処理の基本からチャネルまで

スポンサーリンク

goroutine はGoが標準で持つ並行処理の仕組みです。go キーワードを関数の前につけるだけで並行実行でき、スレッドより軽量なため数千〜数万単位で起動できます。

goroutineを起動する

go をつけて関数を呼ぶとgoroutineが起動します。

package main

import "fmt"

func say(s string) {
    fmt.Println(s)
}

func main() {
    go say("world") // goroutineで実行
    say("hello")    // main goroutineで実行
}

ただしこのままでは main が終了するとgoroutineも強制終了します。goroutineが実行される前に main が終わるため、world が表示されないことがあります。

sync.WaitGroup で完了を待つ

goroutineの終了を待つには sync.WaitGroup を使います。

package main

import (
    "fmt"
    "sync"
)

func say(s string, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println(s)
}

func main() {
    var wg sync.WaitGroup

    wg.Add(1)
    go say("world", &wg)

    wg.Add(1)
    go say("hello", &wg)

    wg.Wait()
    fmt.Println("done")
}
メソッド 説明
wg.Add(n) カウンターを n 増やす
wg.Done() カウンターを 1 減らす
wg.Wait() カウンターが 0 になるまでブロック

defer wg.Done() と書いておくと、関数がどこで終了しても確実に呼ばれます。

チャネル(channel)で値を受け渡す

goroutine間で値をやり取りするにはチャネルを使います。

package main

import "fmt"

func double(x int, ch chan int) {
    ch <- x * 2
}

func main() {
    ch := make(chan int)

    go double(5, ch)

    result := <-ch
    fmt.Println(result) // 10
}

チャネルは送信側と受信側が揃うまでブロックします。

複数のgoroutineから受け取る

package main

import "fmt"

func square(x int, ch chan int) {
    ch <- x * x
}

func main() {
    ch := make(chan int)

    go square(3, ch)
    go square(4, ch)
    go square(5, ch)

    for i := 0; i < 3; i++ {
        fmt.Println(<-ch)
    }
    // 9, 16, 25(順序は不定)
}

バッファ付きチャネル

make(chan int, n) で容量 n のバッファを持つチャネルを作れます。

ch := make(chan int, 3)

ch <- 1
ch <- 2
ch <- 3

fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3

チャネルをクローズする

送信が終わったらチャネルを close します。受信側は range でまとめて受け取れます。

package main

import "fmt"

func generate(ch chan int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

func main() {
    ch := make(chan int)
    go generate(ch)

    for v := range ch {
        fmt.Println(v)
    }
    // 0, 1, 2, 3, 4
}

select で複数チャネルを待つ

select を使うと、複数のチャネルを同時に待って、最初に受け取れたものを処理できます。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println("受信:", msg)
        case msg := <-ch2:
            fmt.Println("受信:", msg)
        }
    }
}
// 受信: one
// 受信: two

default を加えると、どのチャネルも準備できていない場合の処理を書けます。

select {
case msg := <-ch:
    fmt.Println(msg)
default:
    fmt.Println("受信なし")
}

よくある注意点

goroutineリーク

チャネルを受け取らないとgoroutineが永遠に待ち続けてリークします。

// NG: ch を受け取らないのに goroutine を起動
func leak() {
    ch := make(chan int)
    go func() {
        ch <- 1 // 受け取り手がいないため永久にブロック
    }()
}

データ競合(race condition)

複数のgoroutineが同じ変数を読み書きするとデータ競合が起きます。sync.Mutex で保護します。

var mu sync.Mutex
var wg sync.WaitGroup
counter := 0

for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        mu.Lock()
        counter++
        mu.Unlock()
    }()
}
wg.Wait()
fmt.Println(counter) // 1000

競合の検出には go run -race main.go が便利です。

まとめ

仕組み 使い方
goroutine の起動 go 関数名()
終了を待つ sync.WaitGroup
値の受け渡し チャネル chan T
バッファ付きチャネル make(chan T, n)
送信完了を通知 close(ch)
複数チャネルを待つ select
競合検出 go run -race

Goのバージョン確認方法は「Goのバージョン確認方法:go versionコマンドまとめ」を参照してください。

Goのバージョン管理(mise・goenv)は「Goのバージョン管理:miseとgoenvの使い方」を参照してください。