シニア Go エンジニアの面接は、構文のクイズではありません。シニアエンジニアを採用する側は、3 つの節からなる for ループをそらで書けるかどうかも、append が再割り当てを起こしうることを覚えているかどうかも気にしていません。知りたいのは、goroutine リークの原因を突き止められるか、自分のエラー処理のやり方を説明して守れるか、チームが 2 年間付き合っていける API を設計できるか、という点です。質問はあえてオープンエンドになっています。面接官は考え方を見ているのであって、標準ライブラリを暗記しているかを確かめているわけではありません。
このガイドでは、シニアの候補者とミドルレベルの候補者を分けるトピックを、それぞれ実行できるコードとともに解説します。もとにしているのは、現場の Go 開発者が何を作り、何に苦労していると話しているかです。どこかから寄せ集めた「よく出る質問 50 選」のようなリストではありません。
要約
- 対策が最も報われるのは並行処理です。Go 開発者が最も多く作っているのは API/RPC サービス(75%)とコマンドラインツール(62%)で、どちらも goroutine、channel、
contextに大きく依存しています(Go Developer Survey 2024 H2)。 - 課題として最も多く挙がったのは、言語機能の不足ではなく Go らしいコードを書くことです。回答者の 33% が「自分たちの Go のコードがベストプラクティスとイディオムに従っていることを確認すること」を最大の課題に挙げ、これはどの言語機能の不足を挙げた人よりも多い数字でした(2025 Go Developer Survey)。
- データ競合は本番環境で起きるので、面接でも出ると考えておきましょう。Uber ではレースディテクタが Go のモノレポから約 2,000 件のデータ競合を見つけ、210 人のエンジニアが 6 か月かけてそのうち約 1,100 件を修正しました(Uber Engineering)。
errors.Is、errors.As、%wは完璧に使いこなせるようにしておきましょう。回答者の 28% が、他の言語で重宝していた機能が Go にないと答え、その筆頭がエラー処理でした。だから面接官はあなたがエラーをどう扱うかを掘り下げて聞いてきます(2025 Go Developer Survey)。- ジェネリクスは出題範囲に入りますが、落とし穴でもあります。導入された Go 1.18 を、Go チームは「言語史上最大の変更」と呼びました(Go Blog)。シニアらしさが表れるのは、使うべきでない場面を知っていることです。
- 現行のツールチェーンを前提にしましょう。最新の安定版リリースは Go 1.26 です(Go 1.26 リリースノート)。古いバージョンの挙動を説明すると、面接官は気づきます。
目次
- シニア Go エンジニアの面接で実際に問われること
- 並行処理:シニア Go 面接の中心テーマ
- context によるキャンセルとデッドライン
- シニアらしいエラー処理
- ジェネリクス:使うべき場面を見極める
- interface と API 設計
- ライブコーディング課題:レートリミッターを作る
- よくある質問
- シニア Go エンジニアへの道を始めましょう
シニア Go エンジニアの面接で実際に問われること
「Go の面接で最もよく聞かれる質問」をランキングした信頼できる調査はありません。正確な割合を挙げているブログは疑ってかかりましょう。できるのは、Go 開発者が何に時間を使っているかから逆算することです。2024 年の調査では、回答者の 75% が API または RPC サービスを、62% がコマンドラインツールを作っていました(Go Developer Survey 2024 H2)。こうした仕事は並行的で、ネットワークに依存し、失敗のしかたもさまざまです。面接で並行処理、context、エラー処理の話が何度も出てくるのはそのためです。
2025 年の調査では、最大の課題は何かを尋ねています。33% がベストプラクティスとイディオムに従うことを挙げました。28% は、他の言語で重宝していたのに Go にはない機能を挙げ、その多くはエラー処理、直和型、nil ポインタの安全性でした。26% は信頼できるモジュールを見つけることを挙げています(2025 Go Developer Survey)。シニアの面接のかなりの部分は、コミュニティの 3 分の 1 が難しいと感じているイディオムを候補者が身につけているかどうかを、企業が確かめる場です。
つまり、面接で主に試されるのは判断力です。適切な並行処理のプリミティブを選ぶのか、それとも反射的に goroutine を起動するのか。エラーをログに出してそのまま進めるのではなく、ラップした理由を説明できるか。「ここではジェネリクスは使いません」と言い、その根拠を示せるか。
並行処理:シニア Go 面接の中心テーマ
ミドルレベルの候補者がオファーを逃すのは、並行処理の問題です。典型的な失敗は、処理の単位ごとに goroutine を 1 つ起動し、上限もエラーの経路も停止する手段も用意しないことです。シニアの候補者は、上限を設けたパターンから始めます。
goroutine は軽量で、だからこそ素朴なやり方に惹かれてしまいます。Go FAQ にはこうあります。
初期スタックはほとんどのプラットフォームで約 2KB で、ランタイムが管理します。1 つのプログラムで数十万の goroutine を同時に動かせるのはそのためです。とはいえ、軽量だからといってコストがゼロなわけではありません。上限のない goroutine はメモリを食い、下流のサービスにリクエストを殺到させ、リークを覆い隠します。よくある解決策はワーカープールです。
ワーカープール
ワーカープールは、決まった数の goroutine を起動し、channel を通して処理を渡します。「10,000 個の URL を、10,000 個のソケットを同時に開かずに処理してください」という問題への定番の答えです。
example.gogopackage main import ( "fmt" "sync" ) // fetchStatus stands in for any bounded I/O call (HTTP, DB, RPC). func fetchStatus(url string) string { return "200 " + url } func main() { urls := []string{"a", "b", "c", "d", "e"} const workers = 3 jobs := make(chan string) results := make(chan string) var wg sync.WaitGroup for range workers { wg.Go(func() { for url := range jobs { results <- fetchStatus(url) } }) } // Close results once every worker has returned. go func() { wg.Wait() close(results) }() go func() { for _, url := range urls { jobs <- url } close(jobs) }() for r := range results { fmt.Println(r) } }
面接官が注目する点は 3 つあります。jobs を close して、ワーカーの range ループを終わらせること。sync.WaitGroup を使って、results を close してよいタイミングを把握すること。そして、メインのループが受信し切れるように、別の goroutine から results を close することです。この 3 つを押さえれば、全体の仕組みを理解していることを示せます。ツールチェーンの変化を追っていることが伝わる新しい書き方も 2 つあります。for range workers は整数に対する range で、Go 1.22 から使えます。Go 1.25 で追加された wg.Go は、従来の wg.Add(1) と defer wg.Done() の組み合わせを置き換えます。これで、どちらか片方を書き忘れるという WaitGroup の最もよくあるバグがなくなります。
データ競合とレースディテクタ
「このコードの何が問題か」という形で、データ競合を含む問題が出ると考えておきましょう。データ競合は面接の中だけでなく、本番のコードでもしょっちゅう起きます。Uber はその数字を公開しています。同社の Go モノレポには、約 2,100 の独立した Go サービスにわたって約 5,000 万行のコードがあります。レースディテクタは 2,000 件近くのデータ競合を見つけ、210 人のエンジニアが 6 か月でそのうち約 1,100 件を修正しました(Uber Engineering)。
教科書的なデータ競合の例は、共有カウンターです。
example.gogo// BROKEN: concurrent writes to count are a data race. func countBroken(items []int) int { count := 0 var wg sync.WaitGroup for _, n := range items { wg.Add(1) go func() { defer wg.Done() if n%2 == 0 { count++ // unsynchronized write } }() } wg.Wait() return count }
正しい修正方法は、問題の形によって変わります。単純なカウンターなら sync/atomic か sync.Mutex を使います。集計なら channel のほうが向いていることが多く、各 goroutine が結果を送って、1 つの goroutine だけが合計を持つようにします。シニアの回答では、トレードオフを言葉にします。mutex は読みやすく、atomic はロック競合が多い状況で速く、channel(「通信によってメモリを共有する」)なら共有状態そのものをなくせます。そのうえで「go test -race で実行します」と言いましょう。出荷前にデータ競合を捕まえるのは、その習慣です。
ファンアウトとファンイン
ファンアウトは処理を複数の goroutine に分散させ、ファンインはそれらの結果を 1 つの channel にまとめます。1 つの消費者に結果を渡す並列 I/O で使うパターンで、前述のワーカープールとも自然に組み合わせられます。候補者がつまずきやすいのはマージの部分です。マージ先の channel を close するには、ワーカープールの results channel と同じく、送信側に対する WaitGroup が必要です。2 つのパターンがなぜ同じやり方で出力を close するのかを説明できれば、スニペットをなぞっているのではなく、channel の所有権を理解していることを示せます。
context によるキャンセルとデッドライン
Go のサービスは、呼び出しスタック全体に context.Context を渡していきます。面接官は、それを正しく使うよう求めてきます。ルールは、context を最初の引数にすること、名前を ctx にすること、struct には決して保存しないことです。context は、キャンセル、デッドライン、リクエストスコープの値を API の境界を越えて運びます。
最もよくある追加の質問は「遅い呼び出しでブロックしているワーカーを、どうやって止めますか」です。処理と ctx.Done() の両方を select で待ちます。
example.gogofunc process(ctx context.Context, jobs <-chan string) error { for { select { case <-ctx.Done(): return ctx.Err() // context.Canceled or DeadlineExceeded case job, ok := <-jobs: if !ok { return nil // channel closed, work done } if err := handle(ctx, job); err != nil { return fmt.Errorf("handling %q: %w", job, err) } } } }
ctx.Err() を返すと、呼び出し側に停止した理由が伝わり、デッドラインと意図的なキャンセルを区別できます。弱い回答は context を無視するか、ループの先頭でしかチェックしません。それでは handle の中で止まっている goroutine がキャンセルに気づきません。優れた候補者は、リソースを解放するために、親が context.WithCancel や context.WithTimeout から返される cancel 関数を呼ぶ必要があること、通常は defer cancel() で呼ぶことにも触れます。
「context.WithCancel と context.WithTimeout の違いは何ですか」という質問にも備えておきましょう。前者は cancel を呼んだときにキャンセルされます。後者はそれに加えて、デッドラインを過ぎたときにもキャンセルされます。どちらの場合も defer cancel() を書きましょう。context の内部 goroutine をリークさせるのも、リソースリークの一種だからです。
シニアらしいエラー処理
エラー処理は、Go 開発者が他の言語から最も恋しく思う機能です。2025 年には、回答者の 28% が他の言語で重宝していた機能が Go にないと答え、その筆頭がエラー処理でした(2025 Go Developer Survey)。面接官がエラー処理を試すのは、この不満があるからです。面接官が見たいのは、エラーをログに出して忘れるノイズとしてではなく、意味のある値として扱っているかどうかです。
基本は、呼び出し側がチェーンを調べられるように %w でラップすることです。
example.gogovar ErrNotFound = errors.New("not found") func loadUser(ctx context.Context, id string) (*User, error) { row, err := db.Query(ctx, id) if err != nil { // Wrap, don't replace. The caller can still see the root cause. return nil, fmt.Errorf("loadUser %s: %w", id, err) } if row == nil { return nil, fmt.Errorf("loadUser %s: %w", id, ErrNotFound) } return row, nil }
そのうえで、errors.Is はセンチネル値と照合し、errors.As は具体的な型へとアンラップします。
example.gogouser, err := loadUser(ctx, id) if errors.Is(err, ErrNotFound) { http.Error(w, "user not found", http.StatusNotFound) return } var validationErr *ValidationError if errors.As(err, &validationErr) { http.Error(w, validationErr.Field+" is invalid", http.StatusBadRequest) return }
どれをいつ使うかを説明できるようにしておきましょう。呼び出し側がどのエラーが起きたかだけを知ればよいなら、errors.Is とセンチネルが合います。フィールド名やステータスコードのようなデータを取り出す必要があるなら、errors.As と型付きエラーが合います。%w でラップするのは、ラップしたエラーが本当に API の契約の一部である場合に限ります。過剰なラップは実装の詳細を漏らすからです。また、panic はプログラマーのバグや回復不能な起動時の失敗のためのもので、レコードが見つからないといった想定内の状況のためのものではない、とはっきり言いましょう。普通のエラーに panic を使うのは、シニアの選考で落ちる近道です。
ジェネリクス:使うべき場面を見極める
ジェネリクスは Go 1.18 で導入されました。Go チームは次のように発表しています。
面接官がジェネリクスについて尋ねるのは、あなたが使うのを控えられるかを見るためです。正直に答えるなら、ほとんどのコードにジェネリクスは必要なく、よくあるケースは標準ライブラリの slices パッケージと maps パッケージがすでにカバーしている、となります。型パラメータを使うのは、そうしなければ同じロジックを複数の型のためにコピーするか、interface{} に頼って型安全性を失うことになる場合です。
よい使い道の 1 つは、標準ライブラリにまだない、制約付きのヘルパーです。
example.gogoimport "cmp" // Clamp constrains v to the range [lo, hi] for any ordered type. func Clamp[T cmp.Ordered](v, lo, hi T) T { if v < lo { return lo } if v > hi { return hi } return v }
標準の cmp パッケージにある cmp.Ordered 制約は、T を < と > をサポートする型に限定します。どの例を選ぶかからも、あなたのことが伝わります。ジェネリックな Max は書かないでください。Go には 1.21 から組み込みの max と min があり、slice には slices.Max と slices.Min があるので、自前で書くと習慣でジェネリクスに手を伸ばしているように見えます。Clamp は標準ライブラリに同じ役割のものがないので、型パラメータを使う理由があります。ジェネリクス以前は、こうした関数を型ごとにコピーするか、実行時に panic しうる型アサーションと interface{} を組み合わせるしかありませんでした。今はコンパイラがチェックしてくれます。ただし、関数が int しか扱わないなら、int 用に書きましょう。早すぎる一般化は、他の言語と同じく Go でも悪い兆候です。面接官はあなたがそう言うのを待っています。
interface と API 設計
interface に関する質問は設計のセンスを試します。「シニア」の意味の大きな部分はそこにあります。ここで知っておきたい Go の格言は「interface を受け取り、struct を返せ」です。関数は実際に使う最も狭い interface を受け取り、具体的な型を返すべきです。そうすれば、呼び出し側の選択肢が狭まりません。
example.gogo// Good: accepts the minimal behavior it needs. func Copy(dst io.Writer, src io.Reader) (int64, error) { return io.Copy(dst, src) }
Copy は、src がファイルなのか、ネットワーク接続なのか、バッファなのかを気にしません。io.Reader を受け取るので、そのすべてに対応できます。どちらもメソッドが 1 つだけの io.Reader と io.Writer が、この言語で最も再利用されている interface なのはそのためです。
ほかにも伝えておきたい点がいくつかあります。interface は小さく保ち、メソッドは 1 つか 2 つが理想です。パッケージ同士が理由なく依存しないよう、interface は実装する側ではなく利用する側のパッケージで定義します。そして、他の言語でよく見られる、何にでも interface をかぶせる習慣をやめましょう。実装が 1 つしかない struct には、まだ interface は必要ありません。2 つ目の実装やテストダブルが実際に必要になったときに追加しましょう。推測で作った抽象化に面接官は気づきます。イディオムに苦労している 33% が向き合っているのも、そういうコードです(2025 Go Developer Survey)。
ライブコーディング課題:レートリミッターを作る
ライブコーディングでは、小さくても並行処理を含むものを求められるのが一般的です。レートリミッターがよく出るのは、goroutine、channel、時間、後片付けを組み合わせる必要があるからです。time.Ticker を使ったトークンバケット方式のリミッターは、その場で書けるほど短く、話せることもたくさんあります。
example.gogotype Limiter struct { tokens chan struct{} stop chan struct{} } func NewLimiter(perSecond int) *Limiter { l := &Limiter{ tokens: make(chan struct{}, perSecond), // burst capacity stop: make(chan struct{}), } ticker := time.NewTicker(time.Second / time.Duration(perSecond)) go func() { defer ticker.Stop() for { select { case <-ticker.C: select { case l.tokens <- struct{}{}: // refill one token default: // bucket full, drop the tick } case <-l.stop: return } } }() return l } // Allow reports whether a request may proceed right now. func (l *Limiter) Allow() bool { select { case <-l.tokens: return true default: return false } } func (l *Limiter) Close() { close(l.stop) }
コードそのものより、それについてどう話すかのほうが大切です。バッファ付きの tokens channel によって、バースト容量が追加の手間なしで手に入ることを指摘しましょう。default 付きの内側の select が、バケットが満杯のときにブロックせず補充のティックを捨てることを説明しましょう。Close がバックグラウンドの goroutine を止めてリークを防ぐこと、ticker.Stop() がタイマーを解放することにも触れましょう。そのうえで代替案を持ち出します。本番環境では、これを自分で書くより golang.org/x/time/rate を使うのが普通です。独自実装より標準的な解決策を選ぶべき場面を見極めることも、企業が採用で求めている判断力の一部です。
よくある質問
シニア Go エンジニアの面接で最もよく出るトピックは何ですか?
並行処理、context、エラー処理、interface、ジェネリクスで、おおよそこの順番です。この比重は、Go 開発者が作っているものに沿っています。75% が API/RPC サービスを、62% が CLI ツールを書いており(Go Developer Survey 2024 H2)、どちらの仕事も並行的で、失敗のしかたもさまざまです。個々の質問がどれくらい出るかをランキングした信頼できる統計は公開されていないので、そうした数字を示すブログは疑ってかかりましょう。
Go の面接にジェネリクスの知識は必要ですか?
理解しておくべきですが、それ以上に大切なのは、使うべきでない場面を知っておくことです。ジェネリクスは Go 1.18 で言語史上最大の変更として導入されたので(Go Blog)、出題範囲に入ります。ただし、Go らしいコードの多くは今でもジェネリクスを避けています。標準の slices パッケージと maps パッケージでたいていの用途は足りると説明できれば、シニアの候補者として評価が上がります。
レースディテクタはどのくらい重要ですか?
聞かれなくても自分から持ち出すことを面接官が期待するほど重要です。Uber のレースディテクタは、Go のモノレポ全体で 2,000 件近くのデータ競合を見つけ、210 人のエンジニアが 6 か月でそのうち約 1,100 件を修正しました(Uber Engineering)。「go test -race で実行します」と言えば、本番のコードに携わってきたことが面接官に伝わります。
どの Go バージョンを前提にすべきですか?
現行の安定版ツールチェーンです。2026 年初めの時点では Go 1.26 です(Go 1.26 リリースノート)。古いバージョンの挙動を説明すると、知識が古いと思われます。たとえば、整数を range するには 3 つの節からなるループが必要だと言ってしまう場合です。for range n の形は Go 1.22 から使えます。
Go を専門にする価値は今もありますか?
あります。プロの開発者の 17.4% が、過去 1 年間に Go を使って多くの作業をしたと回答しています(Stack Overflow Developer Survey 2025)。公式調査では、回答者の 91% が Go に満足していると答え、63% が非常に満足していると答えました(2025 Go Developer Survey)。シニア Go エンジニアの需要は、この普及に沿って伸びています。
シニア Go エンジニアへの道を始めましょう
こうしたトピックについて読んだからといって、プレッシャーの中で書けるわけではありません。シニアの面接で評価されるのは、体に染みついた力です。考えなくても打てるワーカープール、反射的にラップするエラーチェーン、使わないと判断できるジェネリクス。そこに至るには、現行のツールチェーンで毎日実際の Go を書き、自分のコードが Go らしいかどうかのフィードバックを受けることです。見つけにくいのはそのフィードバックです。しかもイディオムこそ、Go コミュニティの 33% が最も苦労しているものです。
LevelUpGo はそのために作られています。どのレッスンもブラウザ上で取り組む演習で、最新の安定版 Go リリースで実行され、コードが自動で採点されます。ただ読むだけでなく、面接で試されるのと同じ形で練習できます。
この記事の各トピックは、次の場所で練習できます。
- 並行処理、
context、channel:Go Fundamentals コースでは、goroutine、channel、キャンセルを基本原理から積み上げて学びます。無料で始められ、クレジットカードは不要です。 - エラー処理、interface、イディオム:Clean Go Code トラックには、
errors.Is/errors.As、小さな interface、この記事で扱った設計のセンスについての採点付き演習があります。 - LevelUpGo ロードマップ全体を見れば、基礎からシニアレベルの設計まで、コースがどう積み上がっていくかがわかります。
無料のコースを始めて、面接の日まで毎日 Go を書きましょう。そうすれば、当て推量ではなく自分の考えを説明できる状態で面接に臨めます。
