エラーの内容
Goプログラムが実行中にクラッシュし、スタックトレースを出力してステータス2で終了します:
goroutine 1 [running]:
main.main()
/home/user/app/main.go:15 +0x1c
exit status 2
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x47d8a6]
原因はシンプルです:コードがアドレス0x0にあるポインタを通じて読み書きしようとしました。これはGoが「このポインタには実際の値が一度も割り当てられていない」と示す方法です。そのアドレスは常に無効であるため、OSはその場でプロセスを強制終了します。
根本原因
Goはポインタ型、インターフェース、マップ、スライス、チャネル、関数値を自動初期化しません。これらのゼロ値はnilです。実際の値を割り当てる前にそれらに触れると、このパニックが発生します。
実際のコードベースで頻繁に発生する5つのシナリオ:
- nilの構造体ポインタに対してメソッドを呼び出す(
var u *User; u.Name()) - 失敗時に
nilを返す可能性がある関数のエラーを無視する varで宣言したが一度も初期化していないマップやスライスにアクセスする- nilの具象ポインタをラップするインターフェースを格納する
okを確認せずに失敗した型アサーションの結果を使用する
修正1 — 使用前にnilチェックを行う
ポインタやインターフェースに触れる前に必ずガード処理を入れましょう。if文を一つ追加するだけで十分です。
// 悪い例 — respがnilの場合パニックになる
func processResponse(resp *http.Response) string {
return resp.Status
}
// 良い例 — 明示的なnilチェック
func processResponse(resp *http.Response) string {
if resp == nil {
return "no response"
}
return resp.Status
}
修正2 — ポインタを返す関数のエラーを必ず確認する
これはGoにおけるnilパニックの最も一般的な原因です。os.Open、データベースクエリ、JSONアンマーシャルなどの関数はすべて(value, error)を返します。呼び出しが失敗するとvalueはnilになり、エラーを_で無視すれば、一行後にクラッシュが待っています。
// 悪い例 — エラーを無視し、nilでパニックになる
f, _ := os.Open("config.json")
data, _ := io.ReadAll(f) // Openが失敗した場合fはnil
// 良い例 — 先にエラーを確認する
f, err := os.Open("config.json")
if err != nil {
log.Fatal(err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
log.Fatal(err)
}
修正3 — 構造体とマップを使用前に初期化する
varで構造体を宣言すると、そのポインタフィールドはすべてnilのままになります。ネストした構造体に書き込む前に明示的に初期化する必要があります。
// 悪い例 — Config.DBが初期化されていない
type Config struct {
DB *DatabaseConfig
}
var cfg Config
cfg.DB.Host = "localhost" // パニック: DBはnil
// 良い例 — ネストした構造体を初期化する
cfg := Config{
DB: &DatabaseConfig{},
}
cfg.DB.Host = "localhost"
// またはnew()を使う
cfg.DB = new(DatabaseConfig)
cfg.DB.Host = "localhost"
マップでも同じ問題が発生します:
// 悪い例
var cache map[string]int
cache["key"] = 1 // パニック: nilマップへの代入
// 良い例
cache := make(map[string]int)
cache["key"] = 1
修正4 — 型アサーションにはカンマokパターンを使う
型アサーションでokのチェックをスキップすると、型が一致しない瞬間にパニックが発生します。わずか2文字追加するだけでクラッシュを防げます。
// 悪い例
var i interface{} = "hello"
n := i.(int) // パニック: インターフェースの変換エラー
// 良い例
n, ok := i.(int)
if !ok {
fmt.Println("not an int")
return
}
修正5 — スタックトレースを読んで正確な行を特定する
推測は不要です。パニック出力には発生箇所が正確に示されています。調査を始める前にGOTRACEBACK=allを設定しましょう:
GOTRACEBACK=all go run main.go
トレースの中でGoランタイム内部ではなく、自分のパッケージ内の最初のフレームを探してください。そこがnilデリファレンスの箇所です。具体的な例を示します:
panic: runtime error: invalid memory address or nil pointer dereference
goroutine 1 [running]:
main.processUser(...)
/home/user/app/main.go:23 ← 自分のコード、23行目
main.main()
/home/user/app/main.go:10 +0x68
main.goの23行目にジャンプして、nilガードなしに使われているポインタやインターフェースを探してください。
修正6 — recoverは最終手段としてのみ使う
予期しないパニックに耐えなければならないサーバーやミドルウェアを構築していますか?deferとrecoverでリスクのある呼び出しをラップしましょう。ただし、これはあくまでクラッシュ防止ネットであり、修正ではありません。nilの根本原因を突き止めて適切に対処する必要があります。
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
fn()
return nil
}
動作確認
プログラムを再実行してパニックが解消されたことを確認します:
go run main.go
さらに、ユニットテストで修正を固定しておくと、静かにリグレッションが発生するのを防げます:
func TestProcessResponseNil(t *testing.T) {
result := processResponse(nil)
if result != "no response" {
t.Errorf("expected 'no response', got %q", result)
}
}
go test ./...を実行して、パニックなしで緑色の出力が表示されれば修正は成功です。
予防策
- 開発中はレースディテクターを使って実行しましょう:
go run -race main.go— 通常では再現がほぼ不可能な並行nilへの書き込みを検出できます。 - 毎回のコミット前に
go vet ./...を実行してください。一部のnilデリファレンスパターンを静的解析でコストゼロに検出できます。 - staticcheckと
golangci-lintはgo vetが見逃すnilポインタのリスクを検出します。CIに組み込む価値があります。 - 関数が失敗した場合はnilポインタではなく具体的なエラーを返しましょう。nilを暗黙のうちに無視することが不可能になります。
- ヒープ専用の構造体にはコンストラクタを作成しましょう:
func NewUser() *User { return &User{scores: make([]int, 0)} }— 呼び出し側は常に初期化済みの値を受け取れます。

