Golang 入門

Go 構造体フィールドへのアクセス

Go言語において、構造体(Struct)を定義・初期化した後の次のステップは、当然ながらそこに格納されたデータの読み取りや変更です。構造体フィールドへのアクセスは、独自のデータ型を操作する上で最も核心的かつ頻繁に行われるアクションです。

本章では、Go言語における構造体フィールドへのアクセスの全手法を整理します。これには、基礎的なドット演算子によるアクセス、極めて便利なポインタの自動デリファレンス、そして「ネスト(入れ子)」や「コンポジション(匿名ネスト)」を扱う際のフィールドアクセスの特殊なルールが含まれます。

1. 基礎的なアクセス:ドット (.) 演算子の使用

構造体のフィールドにアクセスする最も一般的で直接的な方法は、ドット演算子 (.) を使用することです。構造体のインスタンスがあれば、インスタンス名.フィールド名 という形式で読み取りや変更が可能です。

1.1 基本的な例

最もシンプルな Person 構造体を見てみましょう:

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    person := Person{FirstName: "John", LastName: "Doe", Age: 30}
    
    // ドット演算子を使用してフィールドを読み取る
    fmt.Println("名:", person.FirstName)
    fmt.Println("姓:", person.LastName)
    fmt.Println("年齢:", person.Age)
    
    // ドット演算子を使用してフィールドを変更する
    person.Age = 31
    fmt.Println("誕生日を迎えた後の年齢:", person.Age) // 出力: 31
}

1.2 ネストされた構造体 (Nested Structs) へのチェインアクセス

ある構造体が別の構造体を通常のフィールドとして保持している場合(これをネストと呼びます)、ドット演算子を連続してつなげることで、目的のデータまで辿る「チェインアクセス」が可能です。

package main

import "fmt"

type Address struct {
    Street  string
    City    string
    Country string
}

type Employee struct {
    ID      int
    Name    string
    Address Address // Address を通常のフィールドとしてネスト
}

func main() {
    employee := Employee{
        ID:   123,
        Name: "Alice Smith",
        Address: Address{
            Street:  "123 Main St",
            City:    "Anytown",
            Country: "USA",
        },
    }
    
    // Address 内部のフィールドへチェインアクセス
    fmt.Println("従業員の通り:", employee.Address.Street)
    fmt.Println("従業員の都市:", employee.Address.City)
    
    // ネストされた階層のフィールドを変更
    employee.Address.City = "New York"
    fmt.Println("引っ越し後の都市:", employee.Address.City)
}

2. 高度なアクセス:ポインタによるフィールド操作

以前のポインタの章で触れた通り、関数間で巨大な構造体を渡し、かつ内容を書き換えたい場合は、構造体のポインタを渡す必要があります。では、ポインタを受け取った後、どのようにフィールドにアクセスすればよいのでしょうか。

2.1 Goのスマートな魔法:自動デリファレンス

CやC++では、構造体を指すポインタがある場合、フィールドへのアクセスには特殊な -> 演算子を使用する必要があります。

しかしGo言語では、ポインタに対しても通常のドット演算子 (.) をそのまま使用できます。Goのコンパイラが低レイヤーで極めてスマートに自動デリファレンス(参照解決)を行ってくれるからです。

package main

import "fmt"

type Book struct {
    Title  string
    Author string
    Pages  int
}

func main() {
    book := Book{Title: "Go言語プログラミング", Author: "Alan Donovan", Pages: 384}
    
    bookPtr := &book // bookPtr は book を指すポインタ (*Book)
    
    // bookPtr がポインタであっても、直接 . でフィールドにアクセス可能!
    fmt.Println("書名:", bookPtr.Title)
    fmt.Println("著者:", bookPtr.Author)
    
    // ポインタ経由でフィールドを変更すると、元のインスタンスも変更される
    bookPtr.Pages = 400
    fmt.Println("変更後のページ数:", bookPtr.Pages) // 出力: 400
    fmt.Println("元のインスタンスのページ数も変更済み:", book.Pages) // 出力: 400
}

2.2 厳密な明示的デリファレンス(極めて稀なケース)

Goは自動デリファレンスをサポートしていますが、言語仕様上の厳密な書き方は、まず * を使ってポインタを構造体インスタンスにデリファレンスし、その後に . でフィールドにアクセスするという手順になります。.* よりも優先順位が高いため、括弧を使用して (*pointer).Field と書く必要があります。

package main

import "fmt"

type Car struct {
    Make  string
    Model string
    Year  int
}

func main() {
    car := Car{Make: "Toyota", Model: "Camry", Year: 2022}
    carPtr := &car
    
    // 明示的なデリファレンスの書き方(有効ではあるが、冗長なため実戦で使う人はほぼいない)
    fmt.Println("メーカー:", (*carPtr).Make)
    fmt.Println("モデル:", (*carPtr).Model)
}

結論: (*pointer).Field のことは忘れましょう。常に pointer.Field を使用し、Goが提供するシンプルさを享受してください。

3. コンポジションパターンでのアクセス:匿名フィールドと昇格

前章で学んだ通り、構造体の中で「匿名埋め込み(Anonymous fields)」、つまりフィールド名を付けずに型名だけを記述した場合、埋め込まれた構造体のフィールドは外側の構造体に「昇格 (Promoted)」されます。

これにより、Go言語はオブジェクト指向における「継承」に近い効果を、非常にエレガントな方法で実現しています(Goではこれを「コンポジション Composition」と呼びます)。

3.1 フィールド昇格 (Field Promotion) の例

package main

import "fmt"

type Engine struct {
    Cylinders  int
    Horsepower int
}

type Vehicle struct {
    Make  string
    Model string
    Engine // 匿名埋め込み。フィールド昇格が発生する
}

func main() {
    vehicle := Vehicle{
        Make:  "Ford",
        Model: "Mustang",
        Engine: Engine{
            Cylinders:  8,
            Horsepower: 450,
        },
    }
    
    // Vehicle 自身のフィールドであるかのように、Engine のフィールドに直接アクセスできる!
    fmt.Println("メーカー:", vehicle.Make)
    fmt.Println("シリンダー数:", vehicle.Cylinders) // フィールド昇格が発生
    fmt.Println("馬力:", vehicle.Horsepower)     // フィールド昇格が発生
    
    // もちろん、明示的に階層を指定してアクセスすることも可能
    // fmt.Println(vehicle.Engine.Cylinders) 
}

3.2 同名フィールドの衝突 (Name Collisions) の解決

もし、外側の構造体と匿名埋め込みされた内側の構造体に、たまたま同じ名前のフィールドがあったらどうなるでしょうか。

ルールは明確です:外側のフィールドの優先順位が高くなります(内側の同名フィールドを遮蔽します)。

もし遮蔽された内側のフィールドにアクセスしたい場合は、省略せずにフルパス(階層パス)を記述する必要があります。

package main

import "fmt"

type Inner struct {
    Value int
}

type Outer struct {
    Value int // Inner 内の Value と名前が衝突!
    Inner     // 匿名埋め込み
}

func main() {
    outer := Outer{
        Value: 10, // Outer 自身の Value
        Inner: Inner{
            Value: 20, // Inner の Value
        },
    }
    
    // 直接アクセスすると、外側のフィールドが優先的に取得される
    fmt.Println("outer.Value への直接アクセス:", outer.Value) // 出力: 10
    
    // フルパスを記述することで、遮蔽された内側のフィールドにアクセス可能
    fmt.Println("フルパス outer.Inner.Value でのアクセス:", outer.Inner.Value) // 出力: 20
}