ジュニア Go 面接対策

ジュニア Go Backend 面接質問

基礎を明確かつ自信を持って説明する必要がある Backend Developer 向けに、ジュニア Go の面接質問を 15 問厳選しました。

ジュニア Go の AI 面接を始めるクレジットカードは不要です。1回分の無料セッションがあります。
英語での技術面接練習非ネイティブ話者が技術面接の合格を目指して練習できるモードです。

型システム

1Goにおける組み込み型および参照風の型に対するゼロ値の仕組みと、明示的な初期化を行わずに変数を宣言する際にそれが重要となる理由を説明してください。

Goでは、明示的な初期値なしで宣言された変数は、その型のゼロ値で自動的に初期化されます。数値型は0、bool型はfalse、string型は""になり、配列や構造体は要素ごとまたはフィールドごとにゼロ値で初期化されます。ポインタ、スライス、マップ、チャネル、関数、インターフェイスなどのポインタ風または参照風の型は、nilをゼロ値として持ちます。これが重要な理由は、Goの変数や省略された構造体フィールドが不定なゴミデータを含むことなく確定的な状態で初期化される点にあり、多くのAPIはゼロ値が有用なデフォルト状態となるよう設計されています。ただし、一部のnil値は特定の操作を行う前に初期化が必要です。

package main

import "fmt"

type Config struct {
	Port    int
	Debug   bool
	Name    string
	Tags    []string
	Options map[string]string
}

func main() {
	var c Config
	fmt.Printf("port=%d debug=%v name=%q tags==nil:%v options==nil:%v\n",
		c.Port, c.Debug, c.Name, c.Tags == nil, c.Options == nil)
}
AI コーチを使ってこの質問に答えてみる

2Goは構造体の等値性をどのように処理しますか?また、構造体に比較不可能なフィールドが含まれている場合は何が起きますか?

Goにおける構造体の値は、構造体内のすべてのフィールドが比較可能である場合に限り、`==` および `!=` で比較できます。等値性判定では、各フィールド独自の等値性ルールを使用して、対応するフィールド同士を比較します。構造体にスライス、マップ、関数などの比較不可能なフィールドが含まれている場合、その構造体型は比較不可能となり、その構造体型の2つの値を `==` で比較しようとするとコンパイル時エラーになります。そのような構造体に対しては、カスタムの比較ロジックを実装するか、特にテスト時などには適切なディープイコール(deep-equality)ヘルパー関数を使用します。

package main

import "fmt"

type UserID struct {
    Name string
    Age  int
}

type UserProfile struct {
    Name string
    Tags []string
}

func main() {
    a := UserID{"Ann", 30}
    b := UserID{"Ann", 30}
    fmt.Println(a == b)

    x := UserProfile{Name: "Ann", Tags: []string{"go"}}
    y := UserProfile{Name: "Ann", Tags: []string{"go"}}
    _, _ = x, y
    // fmt.Println(x == y) // compile error: struct containing []string cannot be compared
}
AI コーチを使ってこの質問に答えてみる

3Goにおける文字列の不変性(イミュータビリティ)と、`string`、`[]byte`、`bytes.Buffer`、`strings.Builder` の関係について説明してください。

Goの `string` はバイトの変更不能(イミュータブル)なシーケンスであり、通常はUTF-8テキストですが、必ずしも有効なUTF-8である必要はありません。文字列をその場で(インプレースで)変更することはできません。内容を変更するには、通常、バイト単位の編集であれば `[]byte` に、コードポイント単位の編集であれば `[]rune` に変換し、その後に再変換します。`string` と `[]byte` の通常の変換ではデータがコピーされメモリ割り当てが発生することがあるため、ループ内での繰り返しの変換や結合は高コストになる可能性があります。`strings.Builder` は効率的な文字列構築のために最適化されており、一方 `bytes.Buffer` はバイト指向のデータやI/Oに役立つ変更可能なバイトバッファで、最終的に文字列を生成することもできます。

package main

import (
    "bytes"
    "fmt"
    "strings"
)

func main() {
    var sb strings.Builder
    sb.WriteString("hello")
    sb.WriteByte(' ')
    sb.WriteString("world")
    fmt.Println(sb.String())

    var buf bytes.Buffer
    buf.Write([]byte{0x48, 0x69})
    buf.WriteByte('!')
    fmt.Println(buf.String())

    s := "cat"
    b := []byte(s) // normally copies
    b[0] = 'b'
    fmt.Println(s, string(b))
}
AI コーチを使ってこの質問に答えてみる

4Go におけるポインタの仕組みと、C 言語と比較して意図的に禁止されている操作は何ですか?

Go のポインタは、別の値のアドレスを保持する型付きの値であり、たとえば *int は int を指します。値のアドレスを取得するには &x を使用し、ポインタが指す値を読み書きするには *p でデリファレンス(逆参照)します。ポインタを渡したり保持したりすることで、複数の場所から同じ元データを確認または変更でき、ポインタの値は nil になることもあります。C 言語とは異なり、安全な Go ではポインタ演算や任意のアドレスの直接操作が意図的に禁止されています。このような低レベルの例外的な操作には unsafe パッケージが必要です。

package main

import "fmt"

func inc(p *int) {
    *p = *p + 1
}

func main() {
    x := 10
    p := &x
    inc(p)
    fmt.Println(x)
}
AI コーチを使ってこの質問に答えてみる

5Goにおける定数と iota の仕組みについて、型付き定数と型なし定数の違いを含めて説明してください。

Goの定数はコンパイル時の値であり、真偽値、文字列、または数値です。型付き定数は特定の型を持ちます。型なし定数は型が要求される文脈で使用されるまで固定の具象型を持たず、数値の型なし定数はそれまで高精度で厳密に表現されます(最終的に選択された型で表現可能な値である必要があります)。`iota` は `const` 宣言で使用される事前宣言された識別子です。各constブロックで0から始まり、定数仕様ごとにインクリメントされるため、列挙型のような定数やビットフラグを定義する際に役立ちます。

type Status int

const (
    StatusPending Status = iota // 0, typed Status
    StatusActive                // 1, same expression/type repeated
    StatusClosed                // 2
)

const big = 1 << 100 // untyped integer constant with high precision

var x int64 = 42      // untyped 42 can become int64
// var y int64 = big  // compile error: not representable as int64
AI コーチを使ってこの質問に答えてみる

6Goにおいて明示的な型変換はどのように動作し、なぜこの言語では多くの暗黙的な型変換が避けられているのですか?

Goでは値の型を変更する際、型の変更を常に明示的かつ可視化し、数値、真偽値、文字列の間で意図しない暗黙の変換が発生するのを防ぐため、原則として `T(x)`(例えば `int64(i)` や `MyID(n)`)を用いた明示的な変換が求められます。型変換はGoの言語仕様で定義された変換規則に従う場合のみ許可され、整数オーバーフローや切り捨て、浮動小数点数から整数への0方向への丸め(切り捨て)、精度の喪失などによって値が変化することがあります。一方、型なし定数はより柔軟であり、対象の型で表現可能な値である限り、型付けされたコンテキストで直接使用できます。

var x int64 = 300
var y int8 = int8(x)   // explicit conversion; value changes

var f float64 = 3.9
var i int = int(f)     // truncates toward zero

const c = 42
var n int32 = c        // ok: untyped constant is representable as int32
AI コーチを使ってこの質問に答えてみる

データ構造

7Goにおける配列(array)とスライス(slice)の違いについて、長さ(length)、容量(capacity)、基底ストレージの動作を含めて説明してください。

Goの配列は [3]int のように長さが固定されており、その長さは型の一部です。配列は要素を直接格納し、代入や関数の引数として渡す際には配列全体の値がコピーされます。一方、[]int のようなスライスは、背後にある配列(基底配列)を指す小さな記述子(デスクリプタ)です。概念的には、要素へのポインタ、長さ、容量で構成されています。スライスの長さは参照可能な要素数であり、容量はスライスの開始位置から基底配列の末尾までに利用可能な要素数です。スライスは柔軟であり、再スライスによって記述子が更新されます。また、append を呼び出す際、容量に余裕があれば同じ基底配列を再利用し、不足していれば新しい配列を割り当てます。

package main

import "fmt"

func main() {
	a := [3]int{1, 2, 3}
	b := a
	b[0] = 99
	fmt.Println(a, b)

	s := []int{1, 2, 3}
	t := s
	t[0] = 99
	fmt.Println(s, t)
	fmt.Println(len(s), cap(s))
}
AI コーチを使ってこの質問に答えてみる

8Goのmap型において、キーの型、存在しないキーへのアクセス、nilマップ、および反復処理の順序はどのように動作しますか?

Goのmapのキーの型は比較可能(comparable)である必要があり、スライス、マップ、関数を直接キーとして使用することはできません。存在しないキーを参照すると要素型のゼロ値が返されるため、存在しないことと格納されているゼロ値を区別するにはcomma-ok構文(`v, ok := m[k]`)を使用します。nilマップからの読み出しやrangeによる反復処理は可能ですが、代入を行うとパニックが発生するため、書き込み前に初期化する必要があります。マップの反復順序は未規定(unspecified)であり、コードがその順序に依存してはなりません。

package main

import "fmt"

func main() {
    counts := map[string]int{"a": 0, "b": 2}

    fmt.Println(counts["missing"]) // zero value for int

    v, ok := counts["a"]
    fmt.Println(v, ok) // present even though value is zero

    var m map[string]int
    fmt.Println(m["x"]) // read from nil map is OK
    // m["x"] = 1     // panic: assignment to entry in nil map

    m = make(map[string]int)
    m["x"] = 1

    for k, v := range counts {
        fmt.Println(k, v) // order is not guaranteed
    }
}
AI コーチを使ってこの質問に答えてみる

メモリ管理

9Go において、構造体などの値を値渡しすべきか、ポインタ渡しすべきかはどのように判断すべきですか?

値が小さい場合、呼び出し先で読み取り専用とする場合、または独立したコピーのセマンティクスが必要な場合は値渡しを行います。関数が呼び出し元の値を変更する必要がある場合、コピーのコストが大きい場合、対象の型がコピー禁止である場合、あるいは nil や共有された同一性の表現が API の一部である場合はポインタ渡しを行います。ポインタ渡しが常に高速であると思い込んではいけません。エイリアシングの発生、ヒープへのエスケープ、GC(Garbage Collector)の負荷増加を招き、キャッシュ効率が低下する可能性があるためです。まずは明確なセマンティクスを優先し、性能が重要となる箇所ではベンチマーク測定を行って選択してください。

package main

import "fmt"

type Counter struct{ N int }

func Read(c Counter) int {
    c.N++
    return c.N
}

func Inc(c *Counter) {
    c.N++
}

func main() {
    c := Counter{N: 5}
    fmt.Println(Read(c), c.N)
    Inc(&c)
    fmt.Println(c.N)
}
AI コーチを使ってこの質問に答えてみる

言語セマンティクス

10Goにおいて、構造体(struct)、配列(array)、マップ(map)、スライス(slice)の代入とコピーはどのように動作しますか?

Goにおける代入は代入対象の値をコピーしますが、その効果は型によって異なります。構造体の代入は構造体の各フィールドをコピーし、配列の代入は全要素をコピーします。一方、スライスの代入はポインタ、長さ、キャパシティからなるスライスヘッダーをコピーするため、通常は複数のスライスが同じ内部配列を共有します。マップの代入は参照のようなマップ記述子をコピーするため、両方の変数が同一のマップデータを参照します。独立した所有権を確保したい場合は、スライスに対して`copy`や`slices.Clone`を使用したり、新しいマップを作成してエントリをコピーするなど、防御的コピーを行う必要があります。

package main

import "fmt"

type Point struct{ X int }

func main() {
    a := Point{X: 1}
    b := a
    b.X = 2

    x := [2]int{1, 2}
    y := x
    y[0] = 99

    fmt.Println(a.X, b.X)
    fmt.Println(x, y)
}
AI コーチを使ってこの質問に答えてみる

11Goにおいてクロージャはどのように変数をキャプチャしますか?また、ループやgoroutineに関連してどのようなバグが発生する可能性がありますか?

Goにおけるクロージャとは、自身を囲むレキシカルスコープの変数を参照する関数値のことです。クロージャは変数そのものをキャプチャするため、変数の変更がクロージャ側にも反映され、クロージャがエスケープした場合にはキャプチャされた変数が生成元関数の寿命を超えて生存することがあります。 ループやgoroutineに関連するバグは、変数が変更された後にクロージャがその変数を共有または参照することで発生します。これにより、goroutineや遅延コールバックが意図しない値を参照したり、共有された状態に対してデータ競合(data race)を引き起こしたりすることがよくあります。 一般的な対処法は、意図した値をクロージャの引数として渡すか、イテレーションごとに新しいローカルコピーを作成すること、そして必要に応じて並行アクセスを同期することです。Go 1.22以降では、`for`/`range`で宣言されたループ変数がイテレーションごとのスコープとなったため、ループ変数のキャプチャに起因する典型的なバグの多くは解消されましたが、ループ外で再利用される変数やその他の変更可能な共有状態では依然として問題が発生する可能性があります。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(v int) {
            defer wg.Done()
            fmt.Println(v)
        }(i)
    }
    wg.Wait()
}
AI コーチを使ってこの質問に答えてみる

12配列、スライス、マップ、文字列、チャネルに対する range のセマンティクスについて説明してください。

`range` は対象のオペランドの型に応じて反復処理を行います。配列またはスライスに対してはインデックスと要素の値を返します。要素の値はコピーであるため、代入しても元のコレクションは変更されません。配列の値に対する range は反復処理のために配列全体をコピーしますが、スライスに対する range はスライスヘッダーを使用し、背後にある基底配列をインデックス参照します。マップに対してはキーと値を順不同で返し、値はコピーとなります。文字列に対してはバイトインデックスとデコードされたUnicodeコードポイント(`rune`)を返します(文字位置のインデックスではありません)。チャネルに対しては、チャネルがクローズされ空になるまで値を受信し続けます。なお、nil チャネルに対する range は永久にブロックします。

package main

import "fmt"

func main() {
    xs := []int{1, 2, 3}
    for _, v := range xs {
        v *= 10 // modifies only the copy
    }
    fmt.Println(xs)

    for i := range xs {
        xs[i] *= 10 // modifies the slice element
    }
    fmt.Println(xs)
}
AI コーチを使ってこの質問に答えてみる

メソッド

13値レシーバとポインタレシーバの違いは何ですか?また、レシーバの選択は変更可能性、コピー、インターフェースの充足にどのように影響しますか?

値レシーバを持つメソッドはレシーバのコピーを受け取るため、読み取り専用の振る舞いや、イミュータブルな設計の小さな型に適しています。レシーバのコピーに対する変更は呼び出し元には反映されません。一方、ポインタレシーバを持つメソッドはレシーバへのポインタのコピーを受け取るため、元の値を変更でき、サイズが大きい値やコピーすべきでない値のコピーを回避できます。レシーバの選択は method set(メソッドセット)に影響します。値レシーバのメソッドは T と *T の両方のメソッドセットに含まれますが、ポインタレシーバのメソッドは *T のメソッドセットにのみ含まれます。そのため、ポインタレシーバのメソッドを要求するインターフェースは *T によって充足され、T では充足されません。これは、アドレス取得可能な T の値から通常のメソッド呼び出し構文を使ってそのメソッドを呼び出せる場合であっても同様です。

package main

import "fmt"

type Counter struct{ N int }

func (c Counter) AddValue() { c.N++ }
func (c *Counter) AddPointer() { c.N++ }

func main() {
    c := Counter{N: 1}
    c.AddValue()
    fmt.Println(c.N)
    c.AddPointer() // compiler rewrites to (&c).AddPointer() because c is addressable
    fmt.Println(c.N)
}
AI コーチを使ってこの質問に答えてみる

インターフェイス

14Goにおけるインターフェイスの仕組みと、ある型が暗黙的にインターフェイスを満たす(satisfy)とはどういうことかを説明してください。

Goのインターフェイスは要求されるメソッドのセットを定義します。具象型は、そのメソッドセット(method set)に必要なメソッドが含まれている場合、暗黙的にそのインターフェイスを満たします。`implements` 宣言は存在しません。インターフェイス変数には、そのインターフェイスを満たす型の具象動的値を保持でき、インターフェイスのメソッド呼び出しはその動的な値の実装へとディスパッチされます。`interface{}`、または一般に `any` と記述される空のインターフェイスには要求されるメソッドがないため、すべての型がこれを満たします。`T`、`*T`、またはその両方がインターフェイスを満たすかどうかは、それぞれのメソッドセットとレシーバの選択によって決まります。

package main

import "fmt"

type Greeter interface {
    Greet() string
}

type User struct{ Name string }

func (u User) Greet() string { return "hello, " + u.Name }

func Say(g Greeter) {
    fmt.Println(g.Greet())
}

func main() {
    Say(User{Name: "Ana"}) // no 'implements Greeter' declaration needed
}
AI コーチを使ってこの質問に答えてみる

15インターフェイス値に対する型アサーションと型スイッチはどのように動作しますか?

型アサーションはインターフェイスに格納されている動的な値を検査します。`v := x.(T)`は、インターフェイス値の動的型が`T`であるか、または`T`が動的な値によって実装されているインターフェイスである場合に成功します。戻り値が1つの形式では、失敗時にパニックが発生します。カンマ・ok形式の`v, ok := x.(T)`は、パニックを起こさずに成功の成否を返します。型スイッチである`switch v := x.(type)`は、インターフェイス値の動的型に基づいて分岐し、`v`は一致したcaseに応じた型として扱われます。

func describe(x any) string {
    if s, ok := x.(string); ok {
        return "string: " + s
    }

    switch v := x.(type) {
    case int:
        return fmt.Sprintf("int: %d", v)
    case fmt.Stringer:
        return "stringer: " + v.String()
    case nil:
        return "nil interface"
    default:
        return fmt.Sprintf("unknown %T", v)
    }
}
AI コーチを使ってこの質問に答えてみる