代表的な面接トピック

コーディング面接:Go 1.25のWaitGroup.Goはどのように並行タスクのライフサイクルの正しさを保つか?

コーディング難しい
Offer.cc 編集チーム公開日 更新日

質問

Go 1.25のsync.WaitGroup.Goを用いて並行バッチプロセッサをリファクタリングしてください。カウント、待機、パニック処理、コンテキストキャンセル、エラー収集を正しく行うにはどうすればよいですか?また、どのような場合にerrgroupを引き続き選択しますか?

プロンプトとコンテキスト

バッチプロセッサは多数のオブジェクトを並行して処理します。親goroutineはすべてのタスクを待機し、いずれかのタスクが失敗するかコンテキストがキャンセルされた場合にディスパッチを停止します。ライフサイクルを定義するためにGo 1.25のsync.WaitGroup.Goを使用し、AddDoneWaitとの関係、パニック規約、エラー伝播、およびキャンセル境界について説明してください。中核となるスキルは並行タスクのライフサイクル設計であるため、これはcodingの問題です。

面接官が評価するポイント

第1に、AddWaitと競合する可能性のある場所に配置するのではなく、タスクの生成をカウント処理に適切に結びつけているかどうか。

第2に、WaitGroup.Goの規約を理解しているかどうか:関数はパニックを起こしてはならず、関数がリターンしたときにカウンターがデクリメントされます。エラーやキャンセルの伝播は提供されません。

第3に、リーク、競合、無制限のキューを発生させずに、制限付き並行性の設計、ディスパッチの停止、結果の収集を行えるかどうか。

第4に、WaitGroupを完全なオーケストレーションプリミティブとして扱うよりもerrgroup.WithContextの方が適している場面を特定できるかどうか。

第5に、GoのバージョンとCIの契約を明示しつつ、テストが成功、キャンセル、パニック保護、待機競合をカバーしているかどうか。

最初に明確にすべき質問

  • コンパイラはGo 1.25以降に固定されていますか?
  • 並行数の上限はいくつか、また入力は無制限のストリームになり得ますか?
  • 最初のエラーで他のタスクをキャンセルすべきか、それともすべてのエラーを収集すべきですか?
  • タスクがパニックを起こす可能性はありますか?その場合、どのレイヤーがそれをリカバリしますか?
  • 結果は入力順序を保持する必要がありますか?また、エラーにはオブジェクト識別子が必要ですか?
  • ダウンストリームの呼び出しはコンテキストキャンセルとべき等なリトライをサポートしていますか?

30秒で答える要約

「各起動をそのカウンターに結びつけるためにWaitGroup.Goを使用し、並行数を制限するためにセマフォを使用します。各タスクはコンテキストを確認し、保護されたコレクターを介して結果またはエラーを書き込みます。WaitGroupは待機のみを行い、エラーやキャンセルは伝播しないため、最初のエラーが発生した場合は派生コンテキストを明示的にキャンセルする必要があります。最初のエラーでのキャンセルが標準ポリシーである場合は、errgroup.WithContextを使用します。タスクのエントリポイントでは、許容されたパニックをエラーにリカバリするか、プロセスレベルのポリシーを明示する必要があります。テストではキャンセル、並行数のピーク、収束、競合をカバーします。」

詳細なソリューション

ステップ 1: API規約を固定する

Go 1.25ではsync.WaitGroupGo(f func())が追加されました。これはfを起動し、fがリターンしたときにDoneと同等の処理を実行します。ドキュメントではfがパニックを起こさないことを要求しています。古いコンパイラがワークフローに静かに入り込まないよう、go.mod、CIイメージ、およびローカルツールでバージョンを固定します。

ステップ 2: 並行制限をタスク境界に配置する

Goを呼び出す前、またはタスクの内部でバッファ付きセマフォを使用します。ディスパッチがブロックされる可能性がある場合は、キャンセルされたコンテキストによってディスパッチャが永久に待機し続けないよう、取得処理をキャンセル可能にします。

go
var wg sync.WaitGroup
sem := make(chan struct{}, 8)
for _, item := range items {
  if err := ctx.Err(); err != nil { break }
  sem <- struct{}{}
  item := item
  wg.Go(func() {
    defer func() { <-sem }()
    if ctx.Err() != nil { return }
    process(item)
  })
}
wg.Wait()

ステップ 3: エラーチャネルとキャンセルチャネルを定義する

WaitGroupはエラーを保存せず、兄弟タスクをキャンセルすることもありません。キャンセル可能なコンテキストを派生させ、エラーを記録した最初のタスクがcancelを呼び出すようにします。コレクターにはチャネルまたはミューテックスを使用し、並行アクセスを防ぐためにWaitがリターンした後に読み取ります。

ステップ 4: パニック境界を明示する

サービスがパニックを回復可能なタスク障害として扱う場合は、遅延実行されるrecoverによってスタックトレース付きのエラーに変換し、キャンセルをトリガーできます。パニックが壊れた不変条件を示す場合は、安易にリカバリせず、ロギング、アラート、およびプロセス終了の動作を文書化します。

ステップ 5: ディスパッチと待機の競合を回避する

ライフサイクルプロトコルによって安全性が確保されていない限り、別のgoroutineがGoを呼び出す可能性がある間にWaitを呼び出さないでください。バッチプロセッサでは、単一のディスパッチャがタスクの追加終了を決定してからWaitを呼び出すべきです。動的な再帰タスクでは、ネストされたGo呼び出しがいつ許可されるかを定義する必要があります。

ステップ 6: errgroupとの比較

errgroup.WithContextは、最初の非nilエラー、派生キャンセル、およびオプションの上限を提供するため、1つの失敗で兄弟タスクを停止させるリクエストのファンアウトに適しています。WaitGroup.Goは、独立したタスク、カスタムエラー集約、または別のコンポーネントによって管理されるライフサイクルに適しています。

ステップ 7: 不変条件を検証する

テストでは、起動されたすべてのタスクが最終的にカウントをデクリメントすること、キャンセルによって新しいディスパッチが停止すること、1つのエラーがキャンセルを1回だけトリガーすること、ピークが制限内に収まること、およびWaitの後に結果の変更が停止することを検証する必要があります。コレクターの競合を検出するためにgo test -raceを実行します。

質の高い模範解答

「まずGo 1.25を固定します。ディスパッチャはコンテキストがアクティブな間にセマフォを取得し、wg.Goを呼び出します。タスクはセマフォを解放し、キャンセルを確認して処理を実行します。WaitGroupはライフサイクルの待機のみを提供するため、派生コンテキスト、オブジェクトIDを保持するエラーチャネル、および最初のエラーポリシー用のワンショットキャンセルを追加します。パニックが回復可能である場合はスタック付きのエラーに変換し、そうでない場合はプロセスレベルの処理を明示的に維持します。Waitの前にディスパッチを停止し、その後に結果を集約し、go test -raceを実行します。最初のエラーでのキャンセルと制限を組み合わせる場合は、errgroup.WithContextを選択します。」

よくある間違い

  • WaitGroup.Goをerrgroupとして扱う → エラーが伝播されない → 明示的に収集するかerrgroupを使用する。
  • キャンセル後にセマフォでブロックする → ディスパッチャが終了できない → 取得時にコンテキストに対してselectを使用する。
  • タスクで直接パニックを発生させる → 規約に違反しプロセスが異常終了する可能性がある → 許容されるパニックを変換するか明示的なプロセスポリシーを使用する。
  • 多数のgoroutineから1つのスライスにappendする → データ競合が発生する → チャネル、ミューテックス、または待機後の集約を使用する。
  • ディスパッチ終了前にWaitを呼び出す → 動的な追加が待機と競合する → ディスパッチ停止プロトコルを定義する。
  • 最終カウントのみを検証する → キャンセルや最大並行数のバグが見落とされる → 各不変条件をアサートし競合検出器を実行する。
  • Goのバージョンを無視する → ローカルとCIの挙動が乖離する → モジュール、イメージ、ツールチェーンを固定する。
  • すべてのオーケストレーション要件にWaitGroupを使用する → リトライ、デッドライン、最初のエラーポリシーが分散する → 必要に応じてerrgroupまたは専用のスケジューラを選択する。

フォローアップ質問

フォローアップ 1: Goはパニックを起こす関数を受け取ることができますか?

公式ドキュメントでは、関数がパニックを起こさないことが求められています。パニックが回復可能なビジネス障害である場合は、定義された境界でリカバリしてエラーを返します。それ以外の場合はパニックセマンティクスを維持し、プロセスレベルのリカバリおよびアラートポリシーに依存します。

フォローアップ 2: キャンセル後にタスクが開始されないことをどのように保証しますか?

ディスパッチ前にctx.Err()を確認し、セマフォ取得中にキャンセル可能なselectを使用し、タスク開始時に再度コンテキストを確認します。すでに実行中の処理については、ダウンストリームAPIがキャンセルを尊重するかどうかに依然として依存します。

フォローアップ 3: どのような場合にerrgroupの方が適していますか?

最初のエラー伝播、兄弟タスクのキャンセル、統一された待機が必要な場合はerrgroup.WithContextを選択します。独立したタスクやカスタムエラー集約にはWaitGroup.Goを選択します。

フォローアップ 4: 待機中にGoを呼び出すことはできますか?

新しい処理が追加される前にカウンターがゼロになるのを防ぐライフサイクルプロトコルがある場合に限られます。通常のバッチ処理では、ゼロカウント競合を避けるためにWaitの前にディスパッチを停止する必要があります。

フォローアップ 5: 結果の順序をどのように保持しますか?

各入力にインデックスを割り当て、タスクが自身のスロットに書き込むか、インデックス付きの結果を送信するようにします。待機後にインデックス順に集約します。タスク間でappend専用のスライスを共有してはいけません。

フォローアップ 6: パニックのリカバリをどのようにテストしますか?

パニックを起こすタスクを注入し、リカバリレイヤーが識別されたエラーを出力し、キャンセルをトリガーし、他のタスクを収束させることをアサートします。合意されたモニタリングおよびプロセスポリシーに対して、非リカバリパスを個別にテストします。

公開情報ソース

関連する質問

関連面接ツール

コーディング問題にはスクリーンショットを使用

問題をキャプチャし、制約条件、解法アプローチ、コード、エッジケース、計算量の順に進めます。

ツールを見る