Go面接の頻出25問: 開発者向け完全ガイド
頻出25問でGoの面接を制する。ゴルーチン、チャネル、インターフェース、並行処理パターンをコード例とともに解説。

Goの技術面接では、言語の中核となる概念、すなわち並行処理、メモリ管理、慣用的なパターンへの理解が問われます。本ガイドでは、最も頻出する25問について詳細な回答とコード例をまとめます。
Goは簡潔さと可読性を重視します。面接担当者は、過度に複雑な解よりも深い理解を示す簡潔な回答を好みます。
Go言語の基礎
1. varと:=の違いは何ですか
var宣言は型を明示でき、パッケージレベルでも使えます。:=演算子は型を自動推論しますが、関数内でしか使用できません。
package main
// Package level - var required
var globalConfig = "production"
func main() {
// var with explicit type
var count int = 10
// var with type inference
var name = "Alice"
// Short declaration - functions only
age := 25
// Multiple declarations
var (
host = "localhost"
port = 8080
)
}関数内では簡潔さの観点から:=が好まれますが、パッケージレベルの変数ではvarが必要です。
2. Goの型システムはどのように動作しますか
Goは静的型付けと型推論を採用します。代入時にコピーされる値型と、内部構造を共有する参照型を区別します。
package main
import "fmt"
func main() {
// Value types - full copy
a := [3]int{1, 2, 3}
b := a // Copies the array
b[0] = 100 // Doesn't modify a
fmt.Println(a) // [1 2 3]
// Reference types - share data
slice1 := []int{1, 2, 3}
slice2 := slice1 // Same underlying array
slice2[0] = 100 // Also modifies slice1
fmt.Println(slice1) // [100 2 3]
// Maps are also references
m1 := map[string]int{"a": 1}
m2 := m1
m2["a"] = 100
fmt.Println(m1["a"]) // 100
}配列は値型ですが、スライス、マップ、チャネルは参照型です。
3. 配列とスライスの違いを説明してください
配列はコンパイル時に決まる固定長の型です。スライスは下層の配列に対する動的なビューで、ポインタ・長さ・容量という3つの要素から構成されます。
package main
import "fmt"
func main() {
// Array - fixed size, value type
arr := [5]int{1, 2, 3, 4, 5}
// Slice - view over the array
slice := arr[1:4] // [2 3 4]
fmt.Printf("len=%d, cap=%d\n", len(slice), cap(slice))
// len=3, cap=4
// Modifications affect original array
slice[0] = 20
fmt.Println(arr) // [1 20 3 4 5]
// Direct creation with make
dynamic := make([]int, 3, 10)
// len=3, cap=10
// Append may reallocate
dynamic = append(dynamic, 1, 2, 3, 4, 5)
}Goでは動的なコレクションにはスライスを使うのが一般的です。
4. defer文はどのように動作しますか
deferは関数の終了時に呼び出される処理を予約します。遅延された呼び出しはスタックに積まれ、LIFO(後入れ先出し)の順で実行されます。
package main
import (
"fmt"
"os"
)
func main() {
// LIFO order
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// Prints: 3, 2, 1
}
// Typical use case: resource cleanup
func readFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close() // Always executes
// Read file...
return os.ReadFile(path)
}
// Caution: arguments are evaluated immediately
func deferArgs() {
x := 10
defer fmt.Println(x) // Captures 10
x = 20
// Prints: 10
}deferはpanic発生時にも実行が保証されるため、リソース解放に最適です。
5. Goにおけるインターフェースとは何ですか
インターフェースはメソッドの集合を定義します。これらのメソッドを実装する任意の型は、明示的な宣言なしにインターフェースを暗黙的に満たします。
package main
import "fmt"
// Interface definition
type Writer interface {
Write([]byte) (int, error)
}
// Type that implicitly implements Writer
type FileLogger struct {
path string
}
func (f *FileLogger) Write(data []byte) (int, error) {
// Write to file
fmt.Println("Writing to", f.path)
return len(data), nil
}
// Empty interface - accepts any type
func printAny(v interface{}) {
fmt.Printf("Type: %T, Value: %v\n", v, v)
}
// Type assertion
func process(w Writer) {
// Type check
if fl, ok := w.(*FileLogger); ok {
fmt.Println("FileLogger with path:", fl.path)
}
}インターフェースの暗黙的な実装によりパッケージ間を強く疎結合にできます。
並行処理とゴルーチン
6. ゴルーチンとは何で、スレッドとどう違いますか
ゴルーチンはGoランタイムが管理する軽量スレッドです。スタックは数KB(OSスレッドの数MBに対して)にとどまり、Goのスケジューラが数千のゴルーチンをわずかなOSスレッドに多重化します。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
// Launch 1000 goroutines
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
time.Sleep(100 * time.Millisecond)
fmt.Printf("Goroutine %d finished\n", id)
}(i) // Pass i by value
}
wg.Wait()
fmt.Println("All goroutines completed")
}ループ変数はゴルーチンへ常に値で渡すべきです。さもないと、すべてのゴルーチンが同じ最終値をキャプチャしてしまうことがあります。
7. チャネルの仕組みを説明してください
チャネルはゴルーチン間の通信と同期を可能にします。バッファあり(容量を持つ)とバッファなし(同期)の2種類があります。
package main
import "fmt"
func main() {
// Unbuffered channel - blocks until received
ch := make(chan int)
go func() {
ch <- 42 // Blocks until read
}()
value := <-ch // Receives value
fmt.Println(value)
// Buffered channel - doesn't block until full
buffered := make(chan string, 2)
buffered <- "first"
buffered <- "second"
// buffered <- "third" // Would block
fmt.Println(<-buffered) // "first"
fmt.Println(<-buffered) // "second"
}バッファなしのチャネルは同期を保証し、バッファありのチャネルは送受信の時間的な切り離しを可能にします。
8. 複数のチャネルでselectをどう使いますか
selectは複数のチャネル操作を同時に待機します。最初に準備が整った操作が実行され、同時の場合はランダムに選ばれます。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "from ch2"
}()
// Wait with timeout
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(500 * time.Millisecond):
fmt.Println("Timeout")
}
}
// Non-blocking select with default
select {
case msg := <-ch1:
fmt.Println(msg)
default:
fmt.Println("No message available")
}
}selectはGoで並行処理を優雅に扱う基本ツールです。
9. レースコンディションをどう防ぎますか
レースコンディションは複数のゴルーチンが同期なしで共有データへアクセスしたときに発生します。Goには複数の保護機構があります。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// Solution 1: Mutex
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
// Solution 2: RWMutex for read-heavy workloads
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // Multiple readers allowed
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // Single writer
defer c.mu.Unlock()
c.data[key] = value
}
// Solution 3: atomic for simple counters
var atomicCounter int64
func incrementAtomic() {
atomic.AddInt64(&atomicCounter, 1)
}
func main() {
// Detection: go run -race main.go
counter := SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Count:", counter.count)
}コンパイラフラグ-raceは実行時にレースコンディションを検出します。
10. ワーカープールパターンを説明してください
ワーカープールパターンは、固定数のゴルーチンがキューからタスクを処理することで並行処理を制限します。
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(100 * time.Millisecond) // Simulate work
results <- job * 2
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// Start workers
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Send jobs
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// Wait and close results
go func() {
wg.Wait()
close(results)
}()
// Collect results
for result := range results {
fmt.Println("Result:", result)
}
}このパターンはゴルーチンを増やしすぎることによるメモリやCPUの浪費を防ぎます。
Goの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
エラー処理とpanic/recover
11. Goではどのようにエラーを扱いますか
Goは例外ではなく明示的な戻り値でエラーを表現します。慣例として、errorは戻り値の最後のパラメータです。
package main
import (
"errors"
"fmt"
)
// Sentinel errors for comparison
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("access unauthorized")
)
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation %s: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Message: "must be positive",
}
}
return nil
}
func main() {
// Basic check
if err := validateAge(-5); err != nil {
// Type assertion for custom error
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field: %s\n", valErr.Field)
}
}
// Sentinel error comparison
err := findUser("unknown")
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found")
}
}
func findUser(id string) error {
// Error wrapping with context
return fmt.Errorf("findUser %s: %w", id, ErrNotFound)
}%wによるラップで複数のエラーを連鎖させつつ、元のエラーを判定する余地を残せます。
12. panicとrecoverはいつ使うべきですか
panicは通常の流れを中断してスタックを巻き戻します。recoverはdefer内でpanicを捕捉し、実行を継続させます。
package main
import "fmt"
func safeOperation() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
riskyOperation()
return nil
}
func riskyOperation() {
// Simulates an operation that can panic
panic("something went wrong")
}
// Legitimate use case: initialization validation
func MustCompileRegex(pattern string) *Regexp {
r, err := regexp.Compile(pattern)
if err != nil {
panic(err) // Programming error
}
return r
}
func main() {
err := safeOperation()
if err != nil {
fmt.Println("Recovered error:", err)
}
fmt.Println("Program continues")
}panicはプログラミング上のバグ(不変条件の破綻)に限って使うべきです。想定されるエラー(ファイル欠落、ネットワーク不調)では常にerrorを返すのが望ましいです。
構造体・メソッド・埋め込み
13. 値レシーバとポインタレシーバの違いは何ですか
値レシーバは構造体のコピーを受け取り、ポインタレシーバは参照を受け取って元の構造体を変更できます。
package main
import "fmt"
type Counter struct {
value int
}
// Value receiver - works on copy
func (c Counter) GetValue() int {
return c.value
}
// Pointer receiver - modifies original
func (c *Counter) Increment() {
c.value++
}
// Pointer receiver for large structs (avoids copy)
type LargeStruct struct {
data [1000]int
}
func (l *LargeStruct) Process() {
// Avoids copying 8000 bytes
}
func main() {
c := Counter{value: 0}
c.Increment() // Go automatically converts
fmt.Println(c.GetValue()) // 1
// Careful with interfaces
var _ fmt.Stringer = &c // OK if method on *Counter
}ルール: いずれかのメソッドがポインタレシーバを使う場合は、一貫性のためその型の全メソッドをポインタレシーバに揃えるのが望ましいです。
14. Goにおける埋め込みはどう動きますか
埋め込みはある型を別の型の中に取り込み、メソッドとフィールドを継承します。これは古典的継承ではなくコンポジションです。
package main
import "fmt"
type Logger struct {
prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
// Embedding Logger
type Service struct {
*Logger // Pointer embedding
name string
}
func NewService(name string) *Service {
return &Service{
Logger: &Logger{prefix: name},
name: name,
}
}
func main() {
svc := NewService("API")
// Promoted method - direct access
svc.Log("Starting")
// Explicit access also works
svc.Logger.Log("Explicit")
// Promoted field
fmt.Println(svc.prefix) // "API"
}埋め込みは継承の硬直さを避けつつ柔軟なコンポジションを実現します。
15. Goでシングルトンパターンをどう実装しますか
syncパッケージのsync.Onceは、複数のゴルーチンが同時に呼び出しても初期化を一度だけ実行することを保証します。
package main
import (
"fmt"
"sync"
)
type Database struct {
connectionString string
}
var (
instance *Database
once sync.Once
)
func GetDatabase() *Database {
once.Do(func() {
fmt.Println("Single initialization")
instance = &Database{
connectionString: "postgres://...",
}
})
return instance
}
func main() {
// Concurrent calls - single initialization
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
db := GetDatabase()
fmt.Printf("Instance: %p\n", db)
}()
}
wg.Wait()
}sync.Onceはスレッドセーフで、ダブルチェックロックを伴うミューテックスより簡潔です。
コンテキストとキャンセル
16. contextパッケージの用途は何ですか
contextパッケージは、デッドライン、キャンセルシグナル、リクエストに紐づく値を呼び出しツリー全体で扱います。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// Context with timeout
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel() // Always call cancel
result := make(chan string, 1)
go func() {
// Simulate long operation
time.Sleep(3 * time.Second)
result <- "completed"
}()
select {
case res := <-result:
fmt.Println(res)
case <-ctx.Done():
fmt.Println("Timeout:", ctx.Err())
}
}
// Propagation through functions
func fetchData(ctx context.Context, url string) ([]byte, error) {
// Early check
if ctx.Err() != nil {
return nil, ctx.Err()
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
// HTTP client respects context
resp, err := http.DefaultClient.Do(req)
// ...
}長時間実行になりうる関数はcontext.Contextを最初の引数として受け取るべきです。
17. プログラムのグレースフルシャットダウンをどう実現しますか
SIGINTやSIGTERMといったシステムシグナルを捕捉することで、クリーンに終了できます。
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Context cancelled on signal
ctx, stop := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
)
defer stop()
// Start server
server := startServer()
// Wait for signal
<-ctx.Done()
fmt.Println("\nShutting down...")
// Timeout for graceful shutdown
shutdownCtx, cancel := context.WithTimeout(
context.Background(),
5*time.Second,
)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
fmt.Println("Shutdown error:", err)
}
fmt.Println("Shutdown complete")
}このパターンにより、停止前にアクティブな接続が正常に完了します。
テストとベンチマーク
18. Goではどのようにテストを書きますか
組み込みのtestingパッケージが基本機能を提供します。テストは*_test.goファイルに置きます。
package calculator
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
// Table-driven tests
func TestAddTableDriven(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, -1, -2},
{"mixed", -1, 5, 4},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}複数ケースを試すには、Goの慣用パターンであるテーブル駆動テストが適しています。
19. ベンチマークはどう書きますか
ベンチマークはtesting.Bを使い、go test -benchで実行します。
package main
import (
"strings"
"testing"
)
func BenchmarkStringConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
var s string
for j := 0; j < 100; j++ {
s += "a"
}
}
}
func BenchmarkStringBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 100; j++ {
sb.WriteString("a")
}
_ = sb.String()
}
}
// Typical results:
// BenchmarkStringConcat-8 50000 28000 ns/op
// BenchmarkStringBuilder-8 1000000 1200 ns/opベンチマークは実装間の性能差を浮き彫りにします。
ジェネリクス(Go 1.18以降)
20. Goでジェネリクスをどう使いますか
Go 1.18は型パラメータを導入し、型安全性を保ったまま汎用的なコードを書けるようになりました。
package main
import "fmt"
// Generic function
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
// Custom type constraint
type Number interface {
int | int64 | float64
}
func Sum[T Number](values []T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}
// Generic type
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func main() {
// Usage
doubled := Map([]int{1, 2, 3}, func(n int) int {
return n * 2
})
fmt.Println(doubled) // [2 4 6]
fmt.Println(Sum([]int{1, 2, 3, 4, 5})) // 15
stack := &Stack[string]{}
stack.Push("hello")
stack.Push("world")
val, _ := stack.Pop()
fmt.Println(val) // "world"
}ジェネリクスにより、コードの重複やinterface{}の利用を避けられます。
モジュールと依存関係
21. Goのモジュールシステムはどう動きますか
Goモジュールはセマンティックバージョニングで依存関係を管理します。go.modがモジュールと依存関係を定義します。
module github.com/user/myproject
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/lib/pq v1.10.9
)
// Essential commands:
// go mod init github.com/user/project
// go mod tidy - clean dependencies
// go get package@v1.2.3 - add/update
// go mod vendor - copy locally# Updating dependencies
go get -u ./... # All dependencies
go get -u=patch ./... # Patches onlygo.sumファイルには依存関係の整合性を保証する暗号学的チェックサムが含まれます。
22. Goプロジェクトはどう構成すべきですか
標準的な構成はコミュニティの慣習に従い、厳格なルールは課しません。
myproject/
├── cmd/
│ └── api/
│ └── main.go # Entry point
├── internal/ # Private to module
│ ├── handler/
│ ├── service/
│ └── repository/
├── pkg/ # Reusable external code
├── go.mod
├── go.sum
└── README.mdinternalディレクトリは特別で、その内容は他のモジュールからimportできません。
上級トピック
23. Goのガベージコレクタはどう動きますか
Goは並行・トリカラーマーク&スイープ方式のガベージコレクタを採用し、低レイテンシに最適化されています。
package main
import "runtime"
func main() {
// GC configuration
// GOGC=100 (default) - triggers GC when heap doubles
// Force GC
runtime.GC()
// Memory statistics
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
println("Alloc:", stats.Alloc)
println("NumGC:", stats.NumGC)
println("PauseTotalNs:", stats.PauseTotalNs)
}
// Optimization techniques
// 1. Reuse allocations with sync.Pool
// 2. Pre-allocate slices with make([]T, 0, cap)
// 3. Avoid repeated string/[]byte conversions
// 4. Use pointers for large structs環境変数GODEBUG=gctrace=1はGCのトレースを表示します。
24. Goのスケジューラを説明してください
GoのスケジューラはM:Nモデルを用い、N個のゴルーチンをM個のシステムスレッドへ写像します。G(ゴルーチン)、M(スレッド)、P(論理プロセッサ)の3要素から成り立ちます。
package main
import (
"fmt"
"runtime"
)
func main() {
// Number of logical processors (P)
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
// Number of active goroutines
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
// Yield processor to other goroutines
runtime.Gosched()
// M:P:G model
// - G: goroutine (lightweight stack ~2KB)
// - M: OS thread (machine)
// - P: logical processor (execution context)
//
// Each P has a local queue of Gs
// Work stealing when queue is empty
}スケジューラはGo 1.14以降プリエンプティブで、特定のゴルーチンがPを独占しないようにします。
25. Goでパフォーマンスをどう最適化しますか
最適化はボトルネックを把握するためのプロファイリングから始めます。
package main
import (
"os"
"runtime/pprof"
)
func main() {
// CPU profiling
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Code to profile...
// Memory profiling
mf, _ := os.Create("mem.prof")
defer mf.Close()
pprof.WriteHeapProfile(mf)
}
// Analysis: go tool pprof cpu.prof
// Common optimization techniques:
// 1. Avoid allocations in hot loops
// 2. Use sync.Pool for reusable objects
// 3. Prefer []byte over string for mutations
// 4. Use bufio for I/O
// 5. Batch database operations最適化の前に必ず計測すべきです。プロファイリングは現実のボトルネックについて意外な事実を教えてくれます。
まとめ
これら25問はGoの面接で評価される基本概念をカバーします。
準備チェックリスト:
- ✅ ゴルーチンとチャネルの習熟
- ✅ 暗黙的インターフェースの理解
- ✅ 慣用的なエラー処理
- ✅ contextの正しい使用
- ✅ 並行処理パターン(mutex、ワーカープール)
- ✅ テストとベンチマーク
- ✅ Go 1.18以降のジェネリクス
Goの面接で成功する鍵は、簡潔さと性能のトレードオフを理解し、各並行処理パターンを使い分けるべきタイミングを判断できることです。
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
タグ
共有
関連記事

Go技術面接:Goroutine、Channel、並行処理パターン完全ガイド
Go技術面接で頻出のgoroutine、channel、並行処理に関する質問を網羅。本番レベルのコード例と各回答の背景にある設計思想を2026年の面接対策として解説します。

Goの並行処理: GoroutineとChannel - 完全ガイド
GoroutineとChannelでGoの並行処理を習得しましょう。高度なパターン、同期、select文、ベストプラクティスを詳細なコード例とともに解説します。

Go:Java/Python開発者のための基礎知識 2026年版
JavaやPythonの経験を活かしてGoを素早く習得。ゴルーチン、チャネル、インターフェース、スムーズな移行のための基本パターンを解説。