Go 面接対策

Go Backend Developer の面接質問

Backend Developer 向けに厳選した Go の面接質問を、テーマ別にまとめたものです。EngineerSpeak の練習機能を支える同じ質問カタログから表示しています。

Go Backend の 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において、ポインタ、スライス、マップ、チャネル、関数、インターフェイスにおけるnilの動作の違いを説明してください。

Goでは、nilはポインタ、スライス、マップ、チャネル、関数、およびインターフェイスのゼロ値ですが、それらのnil値に対する操作は型ごとに異なります。nilポインタはnilと比較できますが、デリファレンスするとパニックが発生します。nilスライスは長さと容量が0であり、rangeループでの反復処理やappendが可能です。nilマップは値の読み取りやrangeループでの反復処理が可能ですが、代入を行うとパニックが発生します。nilチャネルに対する送信や受信は永久にブロックし、nilチャネルをクローズするとパニックが発生します。nil関数を呼び出すとパニックが発生します。インターフェイスは動的型と動的値の双方が存在しない場合にのみnilとなります。型付きのnil値(nilポインタなど)を保持しているインターフェイス自体はnilにはなりません。

package main

import "fmt"

type MyErr struct{}

func (*MyErr) Error() string { return "my error" }

func returnsTypedNil() error {
	var e *MyErr = nil
	return e
}

func main() {
	var p *int = nil
	var s []int = nil
	var m map[string]int = nil
	var err error = returnsTypedNil()

	fmt.Println(p == nil)
	fmt.Println(len(s), cap(s), s == nil)
	fmt.Println(m["missing"])
	fmt.Println(err == nil)
}
AI コーチを使ってこの質問に答えてみる

5Goにおける比較可能(comparable)な型とは何ですか?また、比較可能性のルールはマップのキー、等値性比較、およびジェネリクスの制約にどのように影響しますか?

Goにおける比較可能な型とは、`==` および `!=` を使用して値を比較できる型のことです。基本型、ポインタ、チャネル、インターフェイス、ならびにすべてのフィールドまたは要素が比較可能である構造体や配列は比較可能です。一方、スライス、マップ、関数は `nil` との比較を除いて比較可能ではありません。マップのキーは比較可能でなければなりません。等値性判定は各型の比較ルールに従い、インターフェイスの比較は動的な具象値に依存します。比較対象のインターフェイスに比較不可能な動的値が含まれている場合、比較処理はパニック(panic)を引き起こします。ジェネリクスでは、事前宣言された `comparable` 制約を使用することで、型パラメータを `==`/`!=` で比較したり、マップのキーとして使用したりできるようになります。

package main

import "fmt"

type Point struct{ X, Y int }      // comparable
type Bag struct{ Items []string }  // not comparable because of slice field

func Contains[T comparable](xs []T, target T) bool {
    for _, x := range xs {
        if x == target {
            return true
        }
    }
    return false
}

func main() {
    p1, p2 := Point{1, 2}, Point{1, 2}
    fmt.Println(p1 == p2)

    m := map[Point]string{p1: "value"}
    fmt.Println(m[p2])

    fmt.Println(Contains([]string{"a", "b"}, "b"))

    var a any = []int{1}
    var b any = []int{1}
    _, _ = a, b
    // fmt.Println(a == b) // panic: comparing uncomparable type []int
}
AI コーチを使ってこの質問に答えてみる

6Go言語においてbyte、rune、UTF-8(8-bit Unicode Transformation Format)でエンコードされたテキストはどのように表現され、len(s)がユーザーに見える文字数と異なる場合があるのはなぜですか?

Go言語において、`byte`は`uint8`のエイリアスであり1つの生バイトを表し、`rune`は`int32`のエイリアスで1つのUnicodeコードポイントを表します。`string`はバイトの読み取り専用シーケンスであり、通常はUTF-8でエンコードされたテキストですが、任意のバイトを含めることもできます。`len(s)`はruneの数やユーザーに見える文字数ではなく、バイト数を返します。文字列のインデックスアクセスはバイトを返しますが、文字列に対して`range`ループを回すとUTF-8がデコードされ、バイトインデックスとruneが取得されます。`len(s)`が表示上の文字数と一致しない場合があるのは、UTF-8では1つのコードポイントに複数バイトが使用される場合があること、および結合文字や絵文字シーケンスのように1つのユーザーに見える文字が複数のコードポイントで構成される場合があるためです。

package main

import "fmt"

func main() {
    s := "é🙂"

    fmt.Println(len(s))         // bytes: é is 2 bytes, 🙂 is 4 bytes
    fmt.Println(len([]rune(s))) // Unicode code points

    fmt.Printf("first byte: %x\n", s[0])

    for i, r := range s {
        fmt.Printf("byte index %d: %q U+%04X\n", i, r, r)
    }
}
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 コーチを使ってこの質問に答えてみる

9スライスの再スライス(reslice)や代入によって複数のスライスが同じ基底配列を共有する仕組みと、それによって発生しうるバグについて説明してください。

スライス値は、基底配列を指すヘッダーです。スライスの代入や関数への受け渡しでは、要素そのものではなくヘッダーのみがコピーされます。再スライスを行うと、同一の基底配列の特定範囲を指す別のヘッダーが作成されます。そのため、複数のスライスが同じ記憶領域を参照(エイリアス)することになります。一方のスライスを介して要素を変更すると他方のスライスにも反映され、空き容量(キャパシティ)が残っている場合は一方のスライスへの append が他方から見えるデータを上書きしてしまうことがあります。これによって引き起こされるバグには、意図しない値の変更、計算結果の破損、小さな部分スライスの保持による巨大な基底配列のメモリリーク、並行処理でエイリアスを共有することによるデータ競合などが挙げられます。意図しない共有を防ぐには、copy や append([]T(nil), s...) を用いて防御的コピーを行うか、append の前に完全スライス式(full-slice expression)を用いてキャパシティを制限します。

package main

import "fmt"

func main() {
	base := []int{1, 2, 3, 4}
	a := base[:2] // len 2, cap 4
	b := base[2:] // len 2, cap 2

	a[0] = 99
	fmt.Println(base, a, b)

	a = append(a, 77) // reuses base's backing array, overwrites base[2]
	fmt.Println(base, a, b)

	c := append([]int(nil), base[:2]...) // defensive copy
	c[0] = 42
	fmt.Println(base, c)
}
AI コーチを使ってこの質問に答えてみる

10append 実行時におけるスライスの拡張メカニズムを概念レベルで説明し、再割り当ての繰り返しがパフォーマンスに与える影響について述べてください。

append によってスライスに要素を追加する際、十分な容量(capacity)があれば既存の背後にある配列(バッキング配列)へそのまま書き込みます。容量が不足している場合、Go はより大きなバッキング配列を新規に割り当て、既存の要素をコピーした上で新しい要素を書き込み、新しいストレージを指すスライスヘッダを返します。具体的な拡張アルゴリズムは実装依存ですが、概念としては append の繰り返しがならし計算量(amortized time)で効率的になるよう十分なサイズで拡張されます。ただし、再割り当てが頻繁に発生すると、要素のコピーによる CPU 負荷、メモリ割り当ての発生、GC(Garbage Collector)への負荷増加が生じるほか、古いスライスとの参照共有が切れる原因にもなります。必要な要素数が事前に判明している場合は、append で追加するなら make([]T, 0, n)、インデックスで直接格納するなら make([]T, n) のように事前割り当てを行うことで、再割り当てを抑えられます。

package main

func collectNoPrealloc(input []int) []int {
	var out []int
	for _, v := range input {
		out = append(out, v*2)
	}
	return out
}

func collectPrealloc(input []int) []int {
	out := make([]int, 0, len(input))
	for _, v := range input {
		out = append(out, v*2)
	}
	return out
}
AI コーチを使ってこの質問に答えてみる

言語セマンティクス

11Goにおいて、未使用の値の破棄、パッケージのインポート、コンパイル時のインターフェース実装チェックでブランク識別子(blank identifier)はどのように機能しますか?

ブランク識別子 `_` は書き込み専用のプレースホルダです。これに代入すると値は破棄され、再利用可能な変数は作成されません。不要な戻り値やループ変数の無視、`import _ "pkg"` による副作用(初期化処理)のみを目的としたパッケージのインポート、そして `var _ io.Reader = (*MyReader)(nil)` のようなコンパイル時におけるインターフェース実装の検証に使用されます。ブランクインポートでもインポートされたパッケージの初期化処理は実行されます。また、インターフェース検証用の代入では、具象型のメソッドセットがインターフェースを満たしていない場合にコンパイルエラーとなります。

package main

import "fmt"

func lookup() (string, bool) {
    return "gopher", true
}

func main() {
    name, _ := lookup() // ignore the bool
    fmt.Println(name)

    for i, _ := range []int{10, 20} {
        fmt.Println(i)
    }
}
AI コーチを使ってこの質問に答えてみる

パッケージ

12Go におけるパッケージの初期化順序は、init 関数やインポートされた依存関係を含めてどのように動作しますか?

Go は依存関係の順序に従ってパッケージを初期化します。あるパッケージがインポートしている依存パッケージは、そのインポート元パッケージよりも先に初期化されます。パッケージ内では、言語仕様で定義された依存関係および宣言順序に従って変数が初期化され、いかなる `init` 関数よりも先にパッケージレベルの変数が初期化されます。その後、パッケージの `init` 関数が自動的に実行されます。1 つのパッケージに複数の `init` 関数が存在する場合があり、これらを直接呼び出すことはできません。各パッケージは一度だけ初期化されます。実行可能ファイルの場合、まずインポートグラフ全体が初期化され、続いて `main` パッケージが初期化され、最後に `main.main` が呼び出されます。

// a/a.go
package a

import "fmt"

var V = func() int {
    fmt.Println("a var")
    return 1
}()

func init() { fmt.Println("a init") }

// main.go
package main

import (
    "fmt"
    "example/a"
)

var M = func() int {
    fmt.Println("main var", a.V)
    return 2
}()

func init() { fmt.Println("main init") }

func main() { fmt.Println("main") }
AI コーチを使ってこの質問に答えてみる

13Goにおけるパッケージの可視性ルールについて、エクスポートされた識別子と internal/ ディレクトリの規約を含めて説明してください。

Goでは、パッケージの可視性はアクセスキーワードではなく、識別子の命名によって制御されます。名前が大文字のUnicode文字で始まる識別子はエクスポートされ、他のパッケージから参照できます。その他の識別子はエクスポートされず、同じパッケージ内からのみ使用可能です。これは関数、型、メソッド、変数、定数、および構造体のフィールドに適用されます。パッケージはエクスポートされた識別子を使用して公開APIを定義し、実装の詳細は非公開のまま保持します。これとは別に、`internal/` ディレクトリ配下に配置されたパッケージは、その `internal` ディレクトリの親ツリー内にインポートパスを持つコードからのみインポート可能であり、これはGoツールチェーンによって強制されます。

// module example.com/app

// internal/store/store.go
package store

type Client struct { // exported type
    dsn string // unexported field
}

func New(dsn string) *Client { return &Client{dsn: dsn} } // exported
func parseDSN(s string) string { return s }                // unexported

// cmd/api/main.go -- allowed: inside example.com/app tree
package main

import "example.com/app/internal/store"

func main() {
    c := store.New("db")
    _ = c
    // c.dsn is not accessible here: field is unexported.
}

// Code outside example.com/app cannot import example.com/app/internal/store.
AI コーチを使ってこの質問に答えてみる

制御フロー

14Goにおいて defer はどのように動作しますか。実行順序、引数の評価タイミング、および戻り値との関係性を含めて説明してください。

`defer` は、対象の関数を囲む関数が通常の `return` やパニック(panic)による巻き戻しによって終了する際に実行されるよう関数呼び出しを登録します。複数の遅延呼び出しは後入れ先出し(LIFO)の順序で実行されます。遅延対象の関数値と引数は `defer` 文が実行された時点で即座に評価されますが、関数呼び出し自体は後から実行されます。名前付き戻り値を使用している場合、`return` 文が最初に戻り値変数を確定させ、その後に遅延関数が実行されるため、遅延クロージャは呼び出し元に戻り値が渡される前に名前付き戻り値を参照または変更できます。この仕組みにより、`defer` はファイルのクローズ、ミューテックスのアンロック、各種リソースの解放といったクリーンアップ処理に役立ちます。

package main

import "fmt"

func f() (result int) {
    x := 1
    defer fmt.Println("arg evaluated now:", x)
    defer func() {
        result++
        fmt.Println("deferred closure sees x later:", x)
    }()
    x = 2
    return 10
}

func main() {
    fmt.Println("return:", f())
}
AI コーチを使ってこの質問に答えてみる

エラー処理

15Goのエラーハンドリングモデルと、エラーの生成、返却、および判定に関する一般的なプラクティスについて説明してください。

Goではエラーを例外ではなく通常の値として扱います。組み込みの `error` インターフェイスは、`Error() string` メソッドを持つ任意の型によって満たされます。慣例として関数は最後の戻り値として `error` を返し、`nil` は成功を意味し、非nilのエラーは呼び出し元が失敗を処理または伝播させる必要があることを意味します。単純なエラーは通常 `errors.New` で作成され、フォーマットされたエラーは `fmt.Errorf` で作成され、呼び出し元は一般に `if err != nil { ... }` でチェックします。

package main

import (
	"errors"
	"fmt"
)

func findUser(id int) (string, error) {
	if id <= 0 {
		return "", errors.New("invalid user id")
	}
	if id == 42 {
		return "", fmt.Errorf("user %d not found", id)
	}
	return "alice", nil
}

func handler(id int) error {
	name, err := findUser(id)
	if err != nil {
		return fmt.Errorf("find user: %w", err)
	}
	fmt.Println(name)
	return nil
}
AI コーチを使ってこの質問に答えてみる

16Goのバックエンドサービスにおいてパニックからの回復(panic recovery)はどのように処理すべきですか。goroutineがパニックを起こしたときに何が起きるか、プロセスが回復(recover)すべきかクラッシュすべきかの判断基準を含めて説明してください。

パニックが発生すると現在のgoroutineのスタックが巻き戻され、登録されたdeferred関数が実行されます。`recover`は同じgoroutine内のdeferred関数から呼び出された場合にのみ機能し、あるgoroutineが別のgoroutineのパニックを回復することはできません。パニックが回復されない場合、プロセスはクラッシュします。バックエンドサービスでは、リクエストハンドラ、RPCミドルウェア、ワーカーgoroutineのエントリポイントなどの隔離境界に回復処理を配置するのが一般的です。これにより、単一のリクエストやジョブの失敗でサービス全体が停止することを防ぎます。ただし、パニックによって共有状態が破損した可能性があったり、プロセスの整合性が信頼できなくなったりした場合は、盲目的に回復して継続するよりも、プロセスをクラッシュさせて再起動する方が安全です。

func Recover(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("panic: %v\n%s", rec, debug.Stack())
                http.Error(w, "internal server error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}
AI コーチを使ってこの質問に答えてみる

並行処理

17Goにおける nil チャネルとは何ですか?また、誤ってコードを停止させたり、意図的に `select` の `case` を無効化したりする際にどのように機能しますか?

nil チャネルとは値が nil のチャネル変数のことであり、`make` で初期化されていない場合や明示的に nil が代入された場合に発生します。nil チャネルに対する送信や受信は永久にブロックされます。`select` 文内では、nil チャネルを含む `case` は決して準備完了(ready)状態にならないため、チャネル変数に nil を代入することで意図的にその `case` を無効化できます。誤って nil チャネルを使用すると、goroutine がハングしたり、`select` ロジックが期待されるイベントを処理しなくなったりする原因になります。

var in <-chan int = source
for in != nil {
    select {
    case v, ok := <-in:
        if !ok {
            in = nil // disables this receive case
            continue
        }
        fmt.Println(v)
    case <-ctx.Done():
        return
    }
}
AI コーチを使ってこの質問に答えてみる

18sync/atomic におけるアトミック操作はミューテックスによる同期とどのように異なり、どのような場合に適していますか?

sync/atomic は、個別のメモリアドレスに対して load、store、add、swap、compare-and-swap などの不可分な操作を、同期およびメモリ順序の保証とともに提供します。一方、ミューテックスはクリティカルセクションを保護するため、任意のコードや、複数の読み取り・書き込み・フィールドにまたがる不変条件を保護できます。アトミック操作は、カウンタ、フラグ、シーケンス番号、または慎重に設計されたロックフリー構造など、単純で独立した状態に適しています。複数の操作を組み合わせる複合的な処理である場合、複数の値の間で一貫性を保つ必要がある場合、またはアトミック操作による実装の挙動の把握や正当性の証明が困難な場合は、ミューテックスを選択するのが適切です。

package main

import (
    "sync"
    "sync/atomic"
)

var requests atomic.Int64

func recordRequest() {
    requests.Add(1) // one independent counter update
}

type Account struct {
    mu      sync.Mutex
    balance int
    limit   int
}

func (a *Account) Withdraw(n int) bool {
    a.mu.Lock()
    defer a.mu.Unlock()

    // The check and update must be one protected invariant.
    if a.balance-n < -a.limit {
        return false
    }
    a.balance -= n
    return true
}
AI コーチを使ってこの質問に答えてみる

19ゴルーチンリーク(goroutine leak)を防ぐために、チャネルの所有権とゴルーチンのライフサイクルはどのように設計すべきですか?

ゴルーチンは明示的な所有者、明確なシャットダウンシグナル、および保証された終了パスを持つように設計します。原則としてプロデューサー側がチャネル(特に出力チャネル)のクローズを所有し、送信側がまだアクティブである可能性がある間に受信側がチャネルをクローズしてはなりません。ブロッキングを伴う送信、受信、ループ、タイマー、または外部呼び出しは、すべて確実に完了するか、一般的には `context.Context` や done チャネルを介したキャンセルによってブロック解除できるようにする必要があります。送信側が終了した後にのみワーカーの完了を待機しチャネルをクローズできるように、`WaitGroup` や `errgroup` などの協調メカニズムを使用します。

package worker

import (
    "context"
    "sync"
)

type Job int
type Result int

func StartWorkers(ctx context.Context, jobs <-chan Job, n int) <-chan Result {
    results := make(chan Result)

    var wg sync.WaitGroup
    wg.Add(n)
    for i := 0; i < n; i++ {
        go func() {
            defer wg.Done()
            for {
                select {
                case <-ctx.Done():
                    return
                case j, ok := <-jobs:
                    if !ok {
                        return
                    }
                    r := Result(j * 2)
                    select {
                    case results <- r:
                    case <-ctx.Done():
                        return
                    }
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(results) // close after all senders are done
    }()

    return results
}
AI コーチを使ってこの質問に答えてみる

20Go サービスにおける goroutine リークの一般的な原因は何ですか?また、本番環境でそれらをどのように検知し、修正しますか?

Go サービスにおける一般的な goroutine リークの原因としては、チャネルの送受信で永久にブロックされている goroutine、キャンセル処理のない他のブロッキング操作で待機しているもの、デッドラインが設定されていないスタックした I/O、停止しないバックグラウンドのループやティッカー(ticker)、リクエストの寿命を超えて生存するリクエストスコープの goroutine などが挙げられます。本番環境では、goroutine 数の継続的な増加や関連する兆候を監視し、goroutine ダンプや pprof の goroutine プロファイルを調査してどこで停止しているかを特定します。リークを修正するには、それらの goroutine が正常に終了できるようにコードを変更します。具体的には、キャンセル処理やデッドラインの追加、ティッカーの停止、チャネルの適切なクローズ、リクエストから切り離された未追跡の goroutine の排除、必要に応じた並行数の制限を行います。

func handler(w http.ResponseWriter, r *http.Request) {
    go func() {
        result := <-slowCh // may block forever
        _ = result
    }()
}
AI コーチを使ってこの質問に答えてみる