# Go技術面接:Goroutine、Channel、並行処理パターン完全ガイド > Go技術面接で頻出のgoroutine、channel、並行処理に関する質問を網羅。本番レベルのコード例と各回答の背景にある設計思想を2026年の面接対策として解説します。 - Published: 2026-04-01 - Updated: 2026-04-01 - Author: Anthony Fillion-Maillet - Tags: go, golang, interview, goroutines, channels, concurrency - Reading time: 9 min --- Goの技術面接において、goroutine、channel、並行処理に関する質問は最も難度が高いトピックとして知られています。これらの概念を深く理解していることが、シニアGoエンジニアと学習段階の開発者を分ける決定的な要素です。本ガイドでは、2026年の面接で実際に問われる質問を、本番品質のコード例と回答の根拠とともに解説します。 > **面接官が実際にテストするポイント** > > Goの並行処理面接では、goroutineのライフサイクル管理、channelのセマンティクス(バッファ付き vs バッファなし、方向型)、パターンの組み合わせ(fan-out/fan-in、ワーカープール、contextによるキャンセル)の3つの領域が重点的に評価されます。構文の暗記では不十分であり、レースコンディションやデッドロックについて論理的に推論できることが求められます。 ## Goroutineの基礎:面接で必ず問われる質問 面接の最初の段階では、goroutineが実際に何であるかを候補者が理解しているかどうかが確認されます。単に起動方法を知っているだけでは不十分です。 **Q:goroutineとは何ですか。OSスレッドとの違いを説明してください。** goroutineは、OSではなくGoランタイムスケジューラによって管理される軽量な並行関数です。Goランタイムは、M:Nスケジューリングモデル(M個のgoroutineをN個のOSスレッドにマッピング)を使用して、数千のgoroutineを少数のOSスレッド上で多重化します。 ```go // goroutine_basics.go package main import ( "fmt" "runtime" "sync" ) func main() { // Print the number of OS threads available fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) var wg sync.WaitGroup for i := 0; i < 10000; i++ { wg.Add(1) go func(id int) { defer wg.Done() // Each goroutine starts with ~2-8KB stack // OS threads typically start with 1-8MB _ = id }(i) } wg.Wait() fmt.Println("All 10,000 goroutines completed") } ``` 面接で言及すべき主要な違いは以下の通りです。goroutineは2-8KBの動的に成長するスタックで開始されるのに対し、OSスレッドは1-8MBの固定スタックを持ちます。goroutine間のコンテキストスイッチはGoスケジューラによってユーザー空間で処理され、OSスレッドのカーネルレベルのコンテキストスイッチを回避します。このため、10万のgoroutineを起動することは現実的ですが、10万のOSスレッドではシステムリソースが枯渇します。 **Q:goroutineがpanicした場合、何が起きますか。** goroutine内で回復されないpanicは、プログラム全体をクラッシュさせます。JavaやPythonの例外とは異なり、panicはそのgoroutine自身のコールスタックを遡ります(起動元のgoroutineのスタックではありません)。これを捕捉する唯一の方法は、同じgoroutine内のdeferred関数内で`recover()`を使用することです。 ```go // panic_recovery.go package main import "fmt" func safeGo(fn func()) { go func() { defer func() { if r := recover(); r != nil { fmt.Println("recovered from panic:", r) } }() fn() // execute the actual work }() } func main() { safeGo(func() { panic("something went wrong") }) // Give goroutine time to complete select {} } ``` 面接官は、本番環境のGoサービスではgoroutineの起動をリカバリパターンでラップしていることへの認識を求めます。`errgroup`のようなライブラリを使用すれば、より洗練された処理が可能です。 ## Channelのセマンティクス:バッファ付き、バッファなし、方向型 channelに関する質問は、候補者がGoの並行処理モデルを本当に理解しているのか、単にパターンを暗記しているだけなのかを明らかにします。 **Q:バッファ付きchannelとバッファなしchannelの違いは何ですか。** バッファなしchannel(`make(chan T)`)は、送信側と受信側が同時に準備完了している必要があります。送信は、別のgoroutineが受信するまでブロックされます。バッファ付きchannel(`make(chan T, n)`)は、ブロックされることなく最大`n`個の値を送信できます。 ```go // channel_semantics.go package main import "fmt" func main() { // Unbuffered: send blocks until receive is ready ch := make(chan string) go func() { ch <- "hello" // blocks here until main reads }() msg := <-ch fmt.Println(msg) // Buffered: send does not block until buffer is full buf := make(chan int, 3) buf <- 1 // does not block (buffer has space) buf <- 2 // does not block buf <- 3 // does not block // buf <- 4 would block — buffer is full fmt.Println(<-buf, <-buf, <-buf) // 1 2 3 } ``` 面接官がよく追加で尋ねるのは「どちらをいつ選択しますか」という質問です。バッファなしchannelは同期を強制するため、送信側が受信側の処理完了を確認する必要がある場合に適しています。バッファ付きchannelは送信側と受信側のタイミングを分離するため、ワークキューやレート制限など多少の遊びが許容される場面に適しています。 **Q:channelをcloseすると何が起きますか。** channelをcloseすると、これ以上値が送信されないことを通知します。closeされたchannelからの受信はゼロ値を即座に返します。closeされたchannelへの送信はpanicを引き起こします。`range`ループはchannelがcloseされると終了します。 ```go // close_channel.go package main import "fmt" func producer(ch chan<- int, count int) { for i := 0; i < count; i++ { ch <- i } close(ch) // signal: no more values } func main() { ch := make(chan int, 5) go producer(ch, 5) // range exits automatically when channel closes for val := range ch { fmt.Println("received:", val) } // Reading from closed channel returns zero value + false val, ok := <-ch fmt.Printf("after close: val=%d, ok=%v\n", val, ok) } ``` 重要な点として、channelをcloseするのは送信側のみであり、受信側がcloseしてはいけません。別のgoroutineがまだ書き込み中のchannelをcloseすると、panicが発生します。 ## select文:channelの多重化 **Q:`select`はどのように動作しますか。複数のcaseが同時に準備完了した場合はどうなりますか。** `select`文は、いずれかのchannel操作が実行可能になるまでブロックします。複数のcaseが同時に準備完了している場合、Goはランダムに1つを選択します。これにより、特定のcaseが枯渇することを防ぎます。 ```go // select_multiplex.go package main import ( "context" "fmt" "time" ) func fetchFromAPI(ctx context.Context, url string) (string, error) { resultCh := make(chan string, 1) errCh := make(chan error, 1) go func() { // Simulate API call time.Sleep(200 * time.Millisecond) resultCh <- fmt.Sprintf("data from %s", url) }() select { case result := <-resultCh: return result, nil case err := <-errCh: return "", err case <-ctx.Done(): // Context cancelled or timed out return "", ctx.Err() } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() result, err := fetchFromAPI(ctx, "https://api.example.com/data") if err != nil { fmt.Println("error:", err) return } fmt.Println(result) } ``` 面接官が`select`でテストするのは2点です。ランダム選択ルールの理解と、channelを`context.Context`と組み合わせてタイムアウトやキャンセルパターンを実装する能力です。 ## 面接で問われる並行処理パターン シニアレベルのGo面接では、以下のパターンのいずれかをゼロから実装する問題がほぼ必ず出題されます。 ### Fan-Out/Fan-Inパターン **Q:アイテムを並行処理するfan-out/fan-inパイプラインを実装してください。** fan-outは複数のgoroutineに作業を分散します。fan-inは複数のgoroutineからの結果を1つのchannelに集約します。 ```go // fanout_fanin.go package main import ( "fmt" "sync" ) // generator produces values on a channel func generator(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // square reads from input, squares each value func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } // fanIn merges multiple channels into one func fanIn(channels ...<-chan int) <-chan int { var wg sync.WaitGroup merged := make(chan int) for _, ch := range channels { wg.Add(1) go func(c <-chan int) { defer wg.Done() for val := range c { merged <- val } }(ch) } go func() { wg.Wait() close(merged) // close after all inputs are drained }() return merged } func main() { in := generator(2, 3, 4, 5, 6) // Fan out: two goroutines reading from same channel c1 := square(in) c2 := square(in) // Fan in: merge results for result := range fanIn(c1, c2) { fmt.Println(result) } } ``` 面接官が求める重要な洞察は次の点です。`generator`のchannelは`c1`と`c2`の間で共有されるため、各値は正確に1つのワーカーによって処理されます(重複しません)。`fanIn`関数は`WaitGroup`を使用して、全入力channelが排出されたことを確認してからマージされたchannelをcloseします。 ### errgroupによるワーカープール **Q:エラーハンドリング付きの境界付きワーカープールをどのように実装しますか。** `golang.org/x/sync/errgroup`パッケージ(Go拡張標準ライブラリの一部)は、この問題を簡潔に解決します。goroutineのライフサイクル管理、最初のエラーの収集、`context`との統合によるキャンセルを処理します。 ```go // worker_pool.go package main import ( "context" "fmt" "golang.org/x/sync/errgroup" ) func processItem(ctx context.Context, id int) error { // Check for cancellation before heavy work select { case <-ctx.Done(): return ctx.Err() default: } if id == 7 { return fmt.Errorf("failed to process item %d", id) } fmt.Printf("processed item %d\n", id) return nil } func main() { g, ctx := errgroup.WithContext(context.Background()) g.SetLimit(3) // maximum 3 concurrent goroutines for i := 0; i < 10; i++ { id := i g.Go(func() error { return processItem(ctx, id) }) } // Wait blocks until all goroutines finish // Returns the first non-nil error if err := g.Wait(); err != nil { fmt.Println("pipeline error:", err) } } ``` 2026年初頭時点の現行安定版であるGo 1.24でも、このパターンが推奨アプローチです。`SetLimit`メソッドはGo 1.20で追加され、セマフォベースの並行性制限を手動で実装する必要がなくなりました。 ## レースコンディションとsyncプリミティブ **Q:Goでレースコンディションをどのように検出・防止しますか。** Goには、`-race`フラグで有効化される組み込みのレースディテクタが用意されています。実行時に共有メモリへの非同期アクセスを検出します。 ```go // race_condition.go package main import ( "fmt" "sync" "sync/atomic" ) // BAD: race condition — do not use in production func unsafeCounter() int { counter := 0 var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter++ // DATA RACE: concurrent read/write }() } wg.Wait() return counter // result is non-deterministic } // GOOD: atomic operations for simple counters func atomicCounter() int64 { var counter atomic.Int64 var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Add(1) // thread-safe atomic increment }() } wg.Wait() return counter.Load() // always 1000 } // GOOD: mutex for complex shared state type SafeMap struct { mu sync.RWMutex data map[string]int } func (m *SafeMap) Set(key string, val int) { m.mu.Lock() // exclusive lock for writes defer m.mu.Unlock() m.data[key] = val } func (m *SafeMap) Get(key string) (int, bool) { m.mu.RLock() // shared lock for reads defer m.mu.RUnlock() v, ok := m.data[key] return v, ok } func main() { fmt.Println("unsafe:", unsafeCounter()) // unpredictable fmt.Println("atomic:", atomicCounter()) // always 1000 } ``` 面接での回答は3つの同期戦略をカバーする必要があります。複雑な共有状態には`sync.Mutex`/`sync.RWMutex`、単純なカウンタやフラグには`sync/atomic`、goroutine間の通信にはchannel(「メモリを共有して通信するのではなく、通信によってメモリを共有する」)です。`go test -race ./...`の実行は、すべてのCIパイプラインに含まれるべきです。 ## Contextとキャンセルパターン **Q:context.Contextがgoroutineのライフサイクルをどのように制御するか説明してください。** `context`パッケージは、キャンセルシグナル、デッドライン、リクエストスコープの値をgoroutine間で伝播するメカニズムを提供します。長時間実行されるすべてのgoroutineは、最初のパラメータとして`context.Context`を受け取る必要があります。 ```go // context_cancellation.go package main import ( "context" "fmt" "time" ) // worker simulates a long-running task func worker(ctx context.Context, id int, results chan<- string) { select { case <-time.After(time.Duration(id*100) * time.Millisecond): results <- fmt.Sprintf("worker %d: done", id) case <-ctx.Done(): results <- fmt.Sprintf("worker %d: cancelled (%v)", id, ctx.Err()) } } func main() { // Parent context with 250ms deadline ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) defer cancel() results := make(chan string, 5) // Launch 5 workers with increasing durations for i := 1; i <= 5; i++ { go worker(ctx, i, results) } // Collect all results for i := 0; i < 5; i++ { fmt.Println(<-results) } } ``` ワーカー1と2は250msのデッドライン内に完了します。ワーカー3、4、5は`ctx.Done()`を通じてキャンセルシグナルを受信します。このパターンは、GoでレジリエントなHTTPサーバーやマイクロサービスを構築する上で基本となるものです。すべてのリクエストハンドラは、クライアントが切断した際にキャンセルが伝播されるcontextを受け取ります。 > **面接でよくある落とし穴** > > `context.Context`を構造体のフィールドに格納してはいけません。[Go公式ドキュメント](https://pkg.go.dev/context)には「Contextを構造体型の内部に格納しないでください。代わりに、Contextを必要とする各関数に明示的に渡してください」と明記されています。面接官はこの質問で候補者がGoの慣例に従っているかどうかを確認します。 ## デッドロック検出:面接の難問 **Q:このコードはデッドロックしますか。なぜですか。** デッドロックの質問は、候補者がgoroutineのスケジューリングとchannel操作について推論する能力をテストするため、頻出です。 ```go // deadlock_example.go package main func main() { ch := make(chan int) ch <- 42 // DEADLOCK: unbuffered send with no receiver // The main goroutine blocks here forever // Go runtime detects this: "fatal error: all goroutines are asleep" } ``` 修正は単純です。channelをバッファ付きにする(`make(chan int, 1)`)か、送信前に受信用のgoroutineを起動します。Goランタイムは全goroutineがブロックされた場合にデッドロックを検出しますが、**全**goroutineがスリープ状態の場合のみです。1つでもgoroutineが動作中(例:バックグラウンドHTTPサーバー)であれば、ランタイムは部分的なデッドロックを検出しません。 > **部分的なデッドロックは検出されない** > > Goランタイムがデッドロックを検出するのは、プログラム内の全goroutineがブロックされた場合のみです。HTTPサーバーやバックグラウンドワーカーを持つ実際のアプリケーションでは、デッドロック状態のgoroutineリークはランタイムディテクタでは検出されません。本番環境でこれらの問題を診断するには、`pprof`やgoroutineダンプ(`runtime.Stack`)などのツールが必要です。 ## 応用パターン:レート制限付き並行処理 **Q:レート制限付きの並行APIコールをどのように実装しますか。** この質問は、複数の並行処理プリミティブを統合的なソリューションに組み合わせる能力をテストします。 ```go // rate_limited.go package main import ( "context" "fmt" "sync" "time" ) // RateLimiter controls concurrent and temporal access type RateLimiter struct { semaphore chan struct{} // limits concurrency ticker *time.Ticker // limits rate } func NewRateLimiter(maxConcurrent int, interval time.Duration) *RateLimiter { return &RateLimiter{ semaphore: make(chan struct{}, maxConcurrent), ticker: time.NewTicker(interval), } } func (rl *RateLimiter) Execute(ctx context.Context, fn func() error) error { // Wait for rate limit tick select { case <-rl.ticker.C: case <-ctx.Done(): return ctx.Err() } // Acquire concurrency slot select { case rl.semaphore <- struct{}{}: case <-ctx.Done(): return ctx.Err() } defer func() { <-rl.semaphore }() // release slot return fn() } func main() { rl := NewRateLimiter(3, 100*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() err := rl.Execute(ctx, func() error { fmt.Printf("[%v] processing %d\n", time.Now().Format("04:05.000"), id) time.Sleep(150 * time.Millisecond) // simulate work return nil }) if err != nil { fmt.Printf("item %d: %v\n", id, err) } }(i) } wg.Wait() } ``` このパターンは、channelベースのセマフォ(並行数制限)とticker(レート制限)を組み合わせています。contextチェック付きの二重`select`がグレースフルシャットダウンを保証します。このような本番対応の回答が、シニア候補者を際立たせます。 ## まとめ - goroutineはM:Nスケジューリングを持つGoランタイム管理のユーザー空間スレッドであり、起動されたgoroutine内では必ずpanicを回復すべきである - バッファなしchannelは送信側と受信側を同期させ、バッファ付きchannelはタイミングを分離する。送信側が確認を必要とするかどうかで選択する - select文はランダム選択によりchannel操作を多重化する。タイムアウト処理には`context.Context`と組み合わせて使用する - fan-out/fan-inとワーカープール(`errgroup.SetLimit`による)が、面接で最も頻出の並行処理パターンである - 複雑な共有状態には`sync.Mutex`、単純なカウンタには`sync/atomic`、goroutine間通信にはchannelを使用する - データレースの検出にはCIで`go test -race`を必ず実行する。部分的なデッドロックの診断には`pprof`が必要である - `context.Context`を構造体に格納せず、最初の関数パラメータとして渡す - Goのレート制限はchannelセマフォとtickerを組み合わせ、context対応のselect文でラップする --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/go/go-technical-interview-goroutines-channels-concurrency