無料学習ガイド · Goバックエンド面接
Go(Golang)面接の15の質問。 答えの理由まで理解する。
バックエンドエンジニアの面接準備に役立つ15問です。詳しい回答を無料で読み、追加質問を使ってランタイムや並行処理の判断を自分の言葉で説明できるか確かめましょう。
言語仕様上の保証とランタイムの実装詳細を区別しています。バージョンに依存する動作は、使用するGoのリリースで確認してください。
goroutineとランタイム
スケジューラの細部に入る前に、Goが処理をどう実行するかを説明しましょう。
goroutineとOSスレッドはどう違いますか?
goroutineはGoランタイムが管理する実行単位です。ランタイムは多数のgoroutineを、より少数または増減するOSスレッド上にスケジュールします。goroutineが特定のスレッドにずっと固定されるわけではありません。面接では、これが並行処理を扱いやすくする理由を述べたうえで、並行性と同時実行(並列性)を区別しましょう。
追加質問:goroutineがシステムコールでブロックするとどうなりますか?
GoのスケジューラにおけるG・M・Pとは何ですか?
Gはgoroutine、MはOSスレッド、PはGoコードの実行に必要なランタイム資源を表します。通常、MがGを実行するにはPが必要です。G/M/Pは現在のランタイムを理解するためのモデルであり、細かなスケジューリング動作まで言語仕様で保証されているわけではありません。
追加質問:Pの数とGOMAXPROCSにはどんな関係がありますか?
GOMAXPROCSは作成できるgoroutineの数を制限しますか?
いいえ。GOMAXPROCSが制限するのは同時にGoコードを実行できるスレッドの数であり、存在できるgoroutineの数ではありません。goroutineには実行可能・待機中・ブロック中などの状態があります。CPU上の並列実行数と、並行して扱うタスク数を分けて説明しましょう。
追加質問:何千ものgoroutineがあっても、同時に実行中なのは少数なのはなぜですか?
競合があるとき、sync.Mutexはどう動作しますか?
競合がなければ、ロックはアトミック操作で獲得を試みます。複数のgoroutineが競う場合、待機側は短時間スピンした後に休止し、CPUを無期限に消費しないようにします。アンロックにより待機側が実行可能になり、ランタイムの公平性に関する処理は、待機側がいつまでも追い越されることを防ぎます。スピン回数、状態ビット、飢餓状態の閾値はGoのリリースで変わり得る実装詳細です。実務ではクリティカルセクションを短くし、遅いI/Oをロック中に行わず、複雑な方式へ置き換える前に競合を計測します。最初の使用後にMutexをコピーしてはいけません。
追加質問:RWMutexがMutexより遅くなるのはどんな場合ですか?
動作の根拠: Goランタイムのスケジューラ実装 ↗
チャネルと協調処理
ブロックの挙動、所有権、安全な終了処理に注目しましょう。
バッファなしではなく、バッファ付きチャネルを使うのはいつですか?
バッファなしチャネルの送信は、受信側が準備できるまで待ちます。バッファ付きチャネルでは空き容量がある間は送信できるため、その容量の範囲で生産側と消費側を切り離せます。ただし、バッファはバックプレッシャーのかかり方を変えるだけです。無制限のキューにはならず、キャンセルや終了処理の設計も必要です。
追加質問:バッファが満杯になるとどうなりますか?
誰がチャネルを閉じるべきで、受信側には何が見えますか?
通常は送信を所有するgoroutineがチャネルを閉じ、もう値が来ないことを知らせます。閉じた後も受信側はバッファ内の値を取り出せます。バッファが空になった後の受信ではゼロ値とok == falseが返ります。閉じたチャネルへの送信はpanicするため、送信側が複数ある場合は明確な終了手順が必要です。
追加質問:受信側がチャネルを閉じるのは通常なぜ危険ですか?
selectで複数のcaseが実行可能なとき、何が起こりますか?
Goは実行可能な通信caseの中から一つを選びます。特定のcaseが必ず選ばれることや、公平性に依存して正しさを組み立ててはいけません。defaultは通信caseがどれも進めないときだけ実行されます。ループ内で安易に使うとビジースピンを招きます。
追加質問:selectのcaseにnilチャネルを置くとどうなりますか?
リクエストがキャンセルされたとき、ワーカーをどう止めますか?
呼び出し元からcontextを渡し、ブロックし得る処理でctx.Done()を監視します。キャンセルは強制終了ではなく通知です。ワーカー自身がreturnし、呼び出し元は所有するgoroutineの終了も待つ必要があります。ジョブ用チャネルを閉じる責任は送信側に持たせます。
追加質問:チャネル送信でブロックしたgoroutineのリークをどう防ぎますか?
データ競合をどう検出し、修正しますか?
必要な同期なしに並行処理が同じメモリへアクセスし、少なくとも一方が書き込むとデータ競合になります。go test -race ./...で実行時に現れる多くの競合を検出できます。所有権を整理するか、Mutexやチャネルで同期します。ただしrace detectorを通過しただけで、全ての実行経路が競合しないと証明されたわけではありません。
追加質問:データ競合と、処理順序に関する論理バグはどう違いますか?
チャネルは何を管理し、送受信時に何が起こりますか?
チャネルは要素の型とサイズ、バッファ内の要素数と容量、バッファ位置、待機中の送信者と受信者のキューなどを管理します。送信時には待機中の受信者へ値を直接渡すことができます。そうでなければ空きバッファを使うか、通信可能になるまで送信者を休止させます。受信時には待機中の送信者から値を受け取る、バッファから読む、または空で未クローズなら待機する、という動作になります。closeしてもバッファ内の値は失われず、受信側はそれを取り出した後、ゼロ値とok == falseを受け取ります。閉じたチャネルへの送信はpanicします。内部フィールドは理解に役立ちますが、依拠すべき契約は言語レベルのブロックとcloseの規則です。
追加質問:満杯のバッファ付きチャネルへの送信がブロックするのはなぜですか?
組み込みのGoのmapは並行利用しても安全ですか?
どのgoroutineも書き込まない場合、組み込みmapへの並行読み取りは問題ありません。書き込みが読み取りや別の書き込みと重なり得るなら、MutexやRWMutexで保護する、所有者を一つのgoroutineにしてチャネルで通信する、または専用の並行データ構造を使うなど、アクセスを協調させます。テストが通ったことを安全性の証明にしてはいけません。同期されていない並行アクセスはデータ競合です。面接ではまず規則を述べ、「確認してから挿入する」ような複数操作にまたがる不変条件をどう守るか説明しましょう。並行コンテナだけでは複数段階の操作はアトミックになりません。
追加質問:sync.Mapより、mapとMutexの組み合わせが分かりやすいのはどんな場合ですか?
sync.Mapはどんな場合に適していますか?
sync.Mapは並行利用でき、主に「一度書いて何度も読む」と「goroutineごとにほぼ重ならないキーを扱う」という二つのパターン向けに最適化されています。このような負荷ではロック競合を減らせます。それ以外では、特に複数フィールドで一つの不変条件を守る場合、MutexやRWMutexで保護した型付きmapの方が理解しやすいことが多いです。実装には読み取りを最適化した経路と更新用の遅い経路がありますが、内部構造は安定したAPIではありません。暗記したフィールド構成ではなく、負荷と計測結果で選びましょう。最初の使用後にsync.Mapをコピーしてはいけません。
追加質問:自分の負荷でsync.Mapとロック付きmapをどう比較計測しますか?
動作の根拠: Goメモリモデル ↗
GCとメモリ
言語仕様上の保証と、コンパイラやGCの実装を分けて説明しましょう。
Goのガベージコレクションはどう動きますか?
GoのGCは到達可能なヒープオブジェクトを追跡し、到達不能なメモリをマーク&スイープ方式で再利用可能にします。処理の多くはアプリケーションと並行して進みますが、短い停止は残ります。性能を説明する際は「GCがあるから停止しない」と言わず、割り当て速度、生存ヒープ量、レイテンシーを考えましょう。
追加質問:割り当てを減らすとGCのCPU負荷が変わるのはなぜですか?
エスケープ解析とは何ですか?アドレスを取ると必ずヒープに割り当てられますか?
エスケープ解析は、値をgoroutineのスタックフレームに安全に置けるか、より長く生きる領域が必要かをコンパイラが判断する処理です。アドレスを取ることだけでヒープ割り当てになるとは限りません。自分のコードはgo build -gcflags=-m=3 ./...で確認し、重要な性能上の主張はベンチマークやプロファイルでも検証しましょう。
追加質問:Goではポインタを返しても安全なのはなぜですか?
Goは小さなオブジェクトと大きなオブジェクトをどう割り当てますか?
Goランタイムは、多くの小さなヒープ割り当てをサイズクラス別に扱います。Pごとのmcacheはグローバルロックなしで一般的な小オブジェクトの割り当てに応じ、空きが足りなければ中央の構造からspanを取得します。mspanは同じサイズクラスのオブジェクトを含むページを管理し、mheapはページ単位のヒープ割り当てを管理します。極小・小・大オブジェクトで経路が異なり、十分大きい割り当ては小オブジェクト用の高速経路を使いません。正確な閾値やデータ構造はGoのリリースに依存します。まずオブジェクトがエスケープするかを確認しましょう。goroutineのスタックに残る値はヒープ割り当ての経路を通りません。実際のコードの性能は、コンパイラ診断と割り当てプロファイルで検証します。
追加質問:割り当てが多いホットパスがCPU負荷とGC負荷の両方を増やすのはなぜですか?
動作の根拠: Go公式GCガイド ↗
コードから考える
10行で考えるチャネルのclose。
送信側は最後の送信を終えてからチャネルを閉じます。rangeループはバッファ内の二つの値を受け取って終了します。受信側が閉じたらどう変わるでしょうか。
package main
import "fmt"
func main() {
jobs := make(chan int, 2)
jobs <- 1
jobs <- 2
close(jobs) // 送信側が閉じる
for job := range jobs {
fmt.Println(job)
}
}学習を続ける
短い回答はPDFでも読めます。
無料の日本語PDFには14問の短い回答を収録。完全版プレイブックでは、実装の詳細、コード例、追加の面接問題をさらに深く解説します。