HOME > 技術ドキュメント > Go goroutine・channel 並行処理入門
Go 言語 goroutine・channel 並行処理入門
Go 言語の最大の特徴の一つが、軽量スレッドである goroutine と、
goroutine 間の通信手段である channel による並行処理モデルです。
「メモリを共有することで通信するのではなく、通信することでメモリを共有せよ」という
Go の設計思想に基づいています。
本ドキュメントでは基本から実用パターンまでを実例で解説します。
1. goroutine の基本
関数呼び出しの前に go キーワードを付けるだけで goroutine として非同期実行されます。
OS スレッドより桁違いに軽量(初期スタック 2KB 程度)で、数千〜数十万の goroutine を同時に扱えます。
package main
import (
"fmt"
"time"
)
func printNumbers(id int) {
for i := 0; i < 3; i++ {
fmt.Printf("goroutine %d: %d\n", id, i)
time.Sleep(10 * time.Millisecond)
}
}
func main() {
go printNumbers(1)
go printNumbers(2)
// main が終了すると goroutine も終了するため待機
time.Sleep(100 * time.Millisecond)
fmt.Println("done")
}
time.Sleep で待つのは本番コードには不適切です。
後述の sync.WaitGroup や channel で適切に終了を待ちます。
2. channel
channel は goroutine 間で値を安全に受け渡すキューです。
make(chan T) で unbuffered(同期)、make(chan T, n) で buffered(バッファあり)を作ります。
// unbuffered channel(送受信が同期する)
ch := make(chan int)
go func() {
ch <- 42 // 受信側が受け取るまでブロック
}()
val := <-ch // 送信側が送るまでブロック
fmt.Println(val) // 42
// buffered channel(バッファが埋まるまで送信側はブロックしない)
buf := make(chan string, 3)
buf <- "a"
buf <- "b"
buf <- "c"
// buf <- "d" // バッファが満杯なのでブロックする
fmt.Println(<-buf) // "a"
channel の方向を型で制限することで、意図しない操作を防げます。
// chan<- int : 送信専用
func producer(out chan<- int) {
for i := 0; i < 5; i++ {
out <- i
}
close(out) // 送信完了を通知
}
// <-chan int : 受信専用
func consumer(in <-chan int) {
for v := range in { // close されるまでループ
fmt.Println("received:", v)
}
}
func main() {
ch := make(chan int, 5)
go producer(ch)
consumer(ch)
}
3. sync.WaitGroup
複数の goroutine がすべて終了するのを待つには sync.WaitGroup を使います。
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // カウンタをインクリメント
go func(id int) {
defer wg.Done() // 終了時にカウンタをデクリメント
fmt.Printf("worker %d done\n", id)
}(i)
}
wg.Wait() // すべての goroutine が Done() を呼ぶまで待機
fmt.Println("all workers done")
}
4. select 文
select は複数の channel 操作を待ち、最初に準備ができたものを実行します。
タイムアウトやキャンセルの実装に欠かせません。
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
go func() {
time.Sleep(50 * time.Millisecond)
ch1 <- "ch1 ready"
}()
go func() {
time.Sleep(30 * time.Millisecond)
ch2 <- "ch2 ready"
}()
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(100 * time.Millisecond):
fmt.Println("timeout")
}
}
}
// 出力:ch2 ready → ch1 ready(先に受信できた方が先に処理される)
5. context によるキャンセル・タイムアウト
context パッケージは goroutine のキャンセルやデッドラインを伝播させる標準的な仕組みです。
HTTP ハンドラーや外部 API 呼び出しでは必ず使います。
import (
"context"
"fmt"
"time"
)
func slowWork(ctx context.Context) error {
select {
case <-time.After(2 * time.Second):
return nil // 正常終了
case <-ctx.Done():
return ctx.Err() // キャンセル or タイムアウト
}
}
func main() {
// 1秒のタイムアウト
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := slowWork(ctx); err != nil {
fmt.Println("error:", err) // context deadline exceeded
}
}
6. worker pool パターン
goroutine を無制限に起動すると過負荷になります。 worker pool は goroutine の数を固定してタスクを並列処理するパターンです。
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
// 重い処理のシミュレーション
result := job * job
fmt.Printf("worker %d: %d -> %d\n", id, job, result)
results <- result
}
}
func main() {
const numWorkers = 3
const numJobs = 10
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, jobs, results, &wg)
}
// ジョブを投入
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// すべての worker が終わったら results を close
go func() {
wg.Wait()
close(results)
}()
// 結果を収集
var total int
for r := range results {
total += r
}
fmt.Println("total:", total)
}
7. sync.Mutex とデータ競合の検出
共有変数へ複数の goroutine から同時にアクセスする場合は sync.Mutex で保護します。
import "sync"
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
counter := &SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Inc()
}()
}
wg.Wait()
fmt.Println(counter.Value()) // 必ず 1000
}
Go の race detector を使うと、データ競合を実行時に検出できます。
# -race フラグでビルド・テスト go run -race main.go go test -race ./...
読み取り専用のアクセスが多い場合は sync.RWMutex の RLock/RUnlock を使うと
パフォーマンスが向上します。
Go アプリを HTTPS で本番公開する
goroutine を活用した高性能 Go アプリも、本番公開には SSL証明書による HTTPS 化が必須です。
エスロジカルではデジサート・サイバートラストの正規取扱代理店として、
2009年から16年以上、RapidSSL 3,960円/1年(税込)〜でSSL証明書を販売しています。審査サポート・インストール代行も対応しています。
SSL証明書の購入はこちら / Go 言語 Web アプリ入門 / Go Gin REST API 開発入門
