Go の構造体とポインタ - フィールドアクセスの自動デリファレンス

Go では構造体のポインタを扱う機会が非常に多いですが、フィールドアクセスの記法は値の場合とまったく同じです。これは Go のコンパイラが自動デリファレンスを行ってくれるためであり、ポインタを意識しない自然なコードを書ける仕組みになっています。

自動デリファレンスとは

C 言語では、構造体のポインタからフィールドにアクセスするときにアロー演算子 -> を使います。Go にはアロー演算子が存在せず、ドット . だけでアクセスできます。

type User struct {
    Name string
    Age  int
}

u := &User{Name: "Alice", Age: 30}
fmt.Println(u.Name) // Alice
fmt.Println(u.Age)  // 30

u*User 型のポインタですが、u.Name でフィールドにアクセスできています。コンパイラがこれを (*u).Name に自動変換しているためです。明示的にデリファレンスを書いても同じ結果になりますが、実際のコードで (*u).Name と書くことはほぼありません。

Go の記法

ポインタでも値でも u.Name で統一。コンパイラが自動的にデリファレンスする。

C 言語の記法

値は u.name、ポインタは u->name と使い分ける。

フィールドへの代入も同様

読み取りだけでなく、ポインタ経由のフィールドへの代入にも自動デリファレンスが適用されます。

u := &User{Name: "Alice", Age: 30}
u.Age = 31
fmt.Println(u.Age) // 31

u.Age = 31 は内部的に (*u).Age = 31 として処理されます。ポインタが指す先の構造体のフィールドが直接書き換わるため、同じポインタを持つ他の変数からも変更後の値が見えます。

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func main() {
    u := &User{Name: "Alice", Age: 30}
    p := u // p と u は同じ構造体を指すポインタ

    p.Age = 31
    fmt.Println(u.Age) // 31
}

pu は同一の User を指しているため、p 経由の変更が u からも確認できます。

ネストした構造体とポインタ

構造体のフィールドが別の構造体のポインタである場合も、ドットを連鎖させるだけでアクセスできます。

type Address struct {
    City    string
    ZipCode string
}

type Employee struct {
    Name    string
    Address *Address
}
package main

import "fmt"

type Address struct {
    City    string
    ZipCode string
}

type Employee struct {
    Name    string
    Address *Address
}

func main() {
    emp := &Employee{
        Name: "Bob",
        Address: &Address{
            City:    "Tokyo",
            ZipCode: "100-0001",
        },
    }

    fmt.Println(emp.Address.City) // Tokyo
    emp.Address.City = "Osaka"
    fmt.Println(emp.Address.City) // Osaka
}

emp.Address.City という式の中で、emp のデリファレンスと Address のデリファレンスが暗黙に行われています。明示的に書けば (*(*emp).Address).City ですが、Go ではそのような冗長な記述は不要です。

ただし、ネストしたポインタフィールドが nil の場合はパニックが発生します。

emp := &Employee{Name: "Charlie"}
// emp.Address は nil
// emp.Address.City // panic: nil pointer dereference

ネストが深くなるほど nil チェックの必要性が増すため、アクセス前にフィールドが nil でないことを確認する習慣が大切です。

if emp.Address != nil {
    fmt.Println(emp.Address.City)
}

埋め込みフィールドとポインタ

Go の構造体は他の構造体を埋め込む(embed する)ことができます。埋め込みフィールドがポインタ型の場合でも、自動デリファレンスによってフィールドやメソッドに直接アクセスできます。

package main

import "fmt"

type Base struct {
    ID int
}

func (b *Base) PrintID() {
    fmt.Println("ID:", b.ID)
}

type Item struct {
    *Base
    Name string
}

func main() {
    item := Item{
        Base: &Base{ID: 42},
        Name: "Widget",
    }

    fmt.Println(item.ID)  // 42(Base のフィールドに直接アクセス)
    item.PrintID()         // ID: 42(Base のメソッドに直接アクセス)
}

Item*Base を埋め込んでいるため、item.IDBaseID フィールドに到達できます。item.PrintID() も同様で、埋め込まれた *Base のメソッドが昇格(promote)されて Item から直接呼び出せます。

埋め込みフィールドがポインタの場合、ゼロ値では nil になるため注意が必要です。item := Item{Name: "Widget"} のように Base を初期化しなかった場合、item.IDitem.PrintID()nil ポインタのデリファレンスでパニックします。

埋め込みポインタは初期化忘れがバグに直結するため、コンストラクタ関数で確実に初期化するパターンが推奨される。

コンストラクタ関数でのポインタ返却

Go にはクラスやコンストラクタの構文がありませんが、慣習として New〜 という名前の関数がコンストラクタの役割を果たします。多くの場合、構造体のポインタを返します。

func NewEmployee(name, city, zip string) *Employee {
    return &Employee{
        Name: name,
        Address: &Address{
            City:    city,
            ZipCode: zip,
        },
    }
}

ポインタを返すことで、呼び出し元は受け取った値をそのままメソッドに渡したり、他の関数で変更したりできます。構造体のコピーも発生しません。

emp := NewEmployee("Alice", "Tokyo", "100-0001")
emp.Address.City = "Nagoya"
fmt.Println(emp.Address.City) // Nagoya

コンストラクタ関数の中でネストした構造体のポインタまで確実に初期化しておけば、呼び出し元で nil チェックを省略できる場面が増えます。初期化の責任をコンストラクタに集約するのは、Go における構造体設計の定番パターンです。