Go のポインタのゼロ値 nil - 未初期化ポインタの扱い

Go ではすべての型にゼロ値が定められています。int なら 0、string なら ""、bool なら false です。ポインタ型のゼロ値は nil であり、「どこも指していない」状態を表します。

nil とは何か

nil はポインタが有効なメモリアドレスを持っていないことを示す特別な値です。ポインタ変数を初期値なしで宣言すると、自動的に nil が代入されます。

var p *int
fmt.Println(p)        // <nil>
fmt.Println(p == nil) // true

nil は「何も指していない」という状態であって、「ゼロ番地を指している」わけではありません。Go の型システム上、nil はポインタ・スライス・マップ・チャネル・インターフェース・関数の 6 つの型で使われる共通のゼロ値です。

nil ポインタのデリファレンスとパニック

nil ポインタに対して *p のようにデリファレンスを行うと、ランタイムパニックが発生してプログラムが停止します。

package main

func main() {
    var p *int
    _ = *p // panic: runtime error: invalid memory address or nil pointer dereference
}

このパニックは Go のプログラムで遭遇するエラーの中でも特に多いものです。スタックトレースには nil pointer dereference というメッセージが表示されるため、原因の特定自体は難しくありません。問題は、なぜそのポインタが nil のままだったのかを突き止めることにあります。

nil チェックのパターン

ポインタを使う前に nil かどうかを確認するのは、Go における基本的な防御策です。

func printValue(p *int) {
    if p == nil {
        fmt.Println("ポインタが nil です")
        return
    }
    fmt.Println(*p)
}

関数がポインタを受け取る場合、呼び出し元が nil を渡してくる可能性は常にあります。デリファレンスの前にチェックを挟むことで、パニックを未然に防げます。

ただし、すべてのポインタ引数に対して機械的に nil チェックを入れるかどうかは設計判断です。関数の契約として「nil を渡してはならない」と定めるなら、チェックなしでパニックさせたほうがバグの早期発見につながる場合もあります。

不正な呼び出しを静かに無視すると、問題が別の箇所で表面化して原因追跡が困難になるリスクがある。

構造体のポインタフィールドと nil

構造体のフィールドにポインタ型を持たせると、「値がない」という状態を nil で表現できます。

type User struct {
    Name  string
    Email *string
}

Email フィールドが *string 型なので、メールアドレスが登録されていないユーザーは Email: nil として区別できます。string 型のゼロ値は空文字 "" ですが、「空文字が設定されている」のと「そもそも設定されていない」のは意味が異なる場面があります。

package main

import "fmt"

type User struct {
    Name  string
    Email *string
}

func main() {
    u1 := User{Name: "Alice"}

    email := "bob@example.com"
    u2 := User{Name: "Bob", Email: &email}

    printEmail(u1)
    printEmail(u2)
}

func printEmail(u User) {
    if u.Email == nil {
        fmt.Printf("%s: メール未登録\n", u.Name)
        return
    }
    fmt.Printf("%s: %s\n", u.Name, *u.Email)
}

この設計は JSON のデシリアライズとも相性が良く、JSON 側で省略されたフィールドを nil として受け取れます。

nil ポインタでもメソッドは呼べる

Go ではポインタレシーバのメソッドに対して、nil の状態でも呼び出し自体は成功します。

package main

import "fmt"

type Node struct {
    Value int
    Next  *Node
}

func (n *Node) String() string {
    if n == nil {
        return "<nil>"
    }
    return fmt.Sprintf("%d", n.Value)
}

func main() {
    var n *Node
    fmt.Println(n.String()) // <nil>
}

メソッドの中でレシーバが nil かどうかを判定し、適切に処理を分岐させています。連結リストや木構造の実装では、末端を nil で表現し、メソッド内で nil チェックを行うパターンがよく使われます。

nil でパニックするケース

ポインタのデリファレンス(*p)やフィールドアクセス(p.Field)を nil に対して行うとパニックする。

nil でも動作するケース

ポインタレシーバのメソッド呼び出しは nil でも成功する。メソッド内部で nil チェックを行えば安全に処理できる。

nil を返す関数の設計

関数が「結果なし」を返す手段として、ポインタの nil を利用するケースがあります。

func findUser(id int, users []User) *User {
    for i := range users {
        if users[i].Name == fmt.Sprintf("user_%d", id) {
            return &users[i]
        }
    }
    return nil
}

見つからなければ nil を返し、呼び出し側で nil チェックを行います。Go では多値返却で (値, error) を返すパターンが主流ですが、エラーというほどではない「見つからなかった」程度の状況にはポインタの nil が使われることもあります。

nil はポインタの扱いにおいて避けては通れない概念です。「どこも指していない」という状態が存在することを常に意識し、デリファレンスの前に安全確認を行う習慣が、パニックのない堅牢なコードにつながります。