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の使い方」を参照してください。