Golang 入門

Go 構造体の定義

Go言語において、構造体(Struct)を定義することで、複数の関連するデータをまとめた独自のデータ型を作成できます。
構造体を利用することで、現実世界のエンティティや概念を一つのユニットとして抽象化でき、コードの可読性、メンテナンス性、そして効率性を大幅に向上させることが可能です。

1. 構造体 (Struct) とは

構造体(Structure の略)は、複合データ型の一種です。ゼロ個以上の名前付き変数(「フィールド」 Field と呼びます)を組み合わせることができ、各フィールドはそれぞれ独立したデータ型を持つことができます。

構造体は他のオブジェクト指向言語における「クラス (Classes)」に似ていますが、Go言語の構造体はより軽量です。Goは伝統的な継承メカニズムを排除し、データの表現に集中しています(ただし、Goは「コンポジション」という仕組みを通じて強力な再利用性を実現できます。これについては後述します)。

1.1 構造体型の定義

構造体を定義するには、type キーワード、構造体名、そして struct キーワードを使用します。波括弧 {} の内部で、各フィールド(フィールド名とデータ型)を定義します。

package main

// Person という名前の構造体型を定義
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

この例では、FirstName (文字列)、LastName (文字列)、Age (整数) という3つのフィールドを持つ Person 構造体を定義しました。

1.2 構造体のゼロ値 (Zero Values)

Go言語の他のデータ型と同様に、構造体変数を宣言する際に明示的な初期値を割り当てなかった場合、Goは自動的にすべてのフィールドを対応する型のゼロ値で初期化します。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    var p Person
    // %+v を使うと構造体のフィールド名とその値を表示できます
    fmt.Printf("%+v\n", p) // 出力: {FirstName: LastName: Age:0}
}

ご覧の通り、文字列フィールドの FirstNameLastName は空文字列 "" に、整数フィールドの Age0 に初期化されています。

2. 構造体インスタンスの生成と初期化

Goでは、構造体のインスタンスを生成・初期化する方法がいくつかあります。

2.1 構造体リテラル (Struct Literal) の使用

これは開発において最も一般的で直感的なインスタンス化の方法です。波括弧内でフィールド名を指定して具体的な値を割り当てます。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    // 構造体リテラルを使用して初期化
    p := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }
    fmt.Printf("%+v\n", p) // 出力: {FirstName:John LastName:Doe Age:30}
}

トラブル回避ガイド:

フィールド名を省略し、定義時の順序通りに値を入力することも可能ですが、この書き方は全く推奨されません。コードの可読性が著しく低下するだけでなく、将来的に構造体にフィールドが追加されたり順序が変更されたりした場合に、即座にエラーになったり、特定しにくいバグの原因になったりするためです。

// 非推奨の書き方(避けるべき)
p := Person{"John", "Doe", 30}

2.2 new 関数の使用

new 関数はメモリ上に新しい変数のための領域を確保し、それを指すポインタを返します。new で生成された構造体は、すべてのフィールドがゼロ値になります。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    p := new(Person) // *Person (ポインタ) が返されます
    
    fmt.Printf("%+v\n", *p) // 出力: {FirstName: LastName: Age:0}
    
    // フィールドへの値の代入
    p.FirstName = "John"
    p.LastName = "Doe"
    p.Age = 30
    
    fmt.Printf("%+v\n", *p) // 出力: {FirstName:John LastName:Doe Age:30}
}

2.3 コンストラクタ関数 (Constructor Function) の活用

Go言語自体には「コンストラクタ」という専用のキーワードはありませんが、通常の関数を作成することでコンストラクタのような振る舞いを模倣できます。これは、複雑な初期化ロジックが必要な場合や、引数のバリデーション(検証)を行いたい場合に非常に有用です。慣習として、この種の関数は通常 New で始まります。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

// NewPerson はコンストラクタとして機能し、構造体のポインタを返します
func NewPerson(firstName, lastName string, age int) *Person {
    if age < 0 {
        return nil // バリデーション:年齢が不正な場合は nil を返す
    }
    return &Person{
        FirstName: firstName,
        LastName:  lastName,
        Age:       age,
    }
}

func main() {
    p := NewPerson("John", "Doe", 30)
    if p != nil {
        fmt.Printf("生成成功: %+v\n", *p) 
    } 
    
    invalidPerson := NewPerson("Jane", "Doe", -5)
    if invalidPerson == nil {
        fmt.Println("提供された年齢が無効なため、生成に失敗しました") 
    }
}

構造体のポインタ (*Person) を返すのは良い習慣です。これにより、生成失敗時に nil を返せるだけでなく、構造体全体をコピーすることによるパフォーマンスコストも回避できます。

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

3.1 ドット (.) 演算子によるアクセス

ドット演算子を使用することで、構造体内のフィールドに簡単にアクセスしたり、値を変更したりできます。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    p := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }
    
    fmt.Println(p.FirstName) // 出力: John
    fmt.Println(p.LastName)  // 出力: Doe
    
    p.Age = 31               // フィールドの値を変更
    fmt.Println(p.Age)       // 出力: 31
}

3.2 ポインタ経由のアクセス (自動デリファレンス)

構造体を指すポインタを持っている場合でも、同様にドット演算子を使用してフィールドにアクセスできます。Go言語は低レイヤーで自動的にポインタのデリファレンス(参照解決)を行ってくれるため、非常に便利です。

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    // Person のポインタを取得
    p := &Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }
    
    // Go は自動的に p.FirstName を (*p).FirstName として解釈します
    fmt.Println(p.FirstName) // 出力: John
    
    p.Age = 31               // ポインタ経由でフィールドを変更
    fmt.Println(p.Age)       // 出力: 31
}

4. 構造体のネストとコンポジション

Go言語には継承はありませんが、構造体の中に別の構造体をネスト(入れ子に)することで、強力で柔軟な「コンポジション (Composition)」パターンを実現できます。既存のデータ構造を積み木のように組み合わせて、より複雑なモデルを構築できます。

4.1 構造体フィールドのネスト

package main

import "fmt"

type Address struct {
    Street  string
    City    string
    ZipCode string
}

type Person struct {
    FirstName string
    LastName  string
    Age       int
    Address   Address // Address 構造体を通常のフィールドとしてネスト
}

func main() {
    p := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
        Address: Address{
            Street:  "メインストリート 123号",
            City:    "〇〇市",
            ZipCode: "12345",
        },
    }
    
    fmt.Println(p.FirstName)         // 出力: John
    fmt.Println(p.Address.Street)    // 階層を追ってアクセス: 出力: メインストリート 123号
    fmt.Println(p.Address.City)      // 出力: 〇〇市
}

4.2 匿名フィールドとフィールドの昇格

さらに高度な手法として「匿名ネスト」があります。フィールド名を付けずに、他の構造体の型名だけを記述します。
この場合、ネストされた構造体の内部フィールドは外側の構造体に「昇格 (Promoted)」され、外側の構造体から直接アクセスできるようになります。

package main

import "fmt"

type Address struct {
    Street  string
    City    string
    ZipCode string
}

type Person struct {
    FirstName string
    LastName  string
    Age       int
    Address   // 匿名ネスト:型名のみ記述
}

func main() {
    p := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
        Address: Address{
            Street:  "メインストリート 123号",
            City:    "〇〇市",
            ZipCode: "12345",
        },
    }
    
    fmt.Println(p.FirstName)   // 出力: John
    
    // ✨ 魔法が起きました:Street と City が昇格し、p から直接アクセス可能!
    fmt.Println(p.Street)      // 出力: メインストリート 123号
    fmt.Println(p.City)        // 出力: 〇〇市
    
    // もちろん、フルパスでのアクセスも可能です
    // fmt.Println(p.Address.Street) 
}

注意:内側と外側の構造体で同名のフィールドが存在する場合(名前の衝突)、外側のフィールドが優先されます。コードの明確さを保つため、名前の衝突は極力避けることをお勧めします。

5. 実践的なユースケースのデモンストレーション

例 1:長方形を表現する

package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

func main() {
    r := Rectangle{
        Width:  10.0,
        Height: 5.0,
    }
    fmt.Println("幅:", r.Width)   // 出力: 幅: 10
    fmt.Println("高さ:", r.Height)  // 出力: 高さ: 5
}

例 2:本とその著者情報を表現する (コンポジション)

package main

import "fmt"

type Author struct {
    Name string
    Bio  string
}

type Book struct {
    Title  string
    Author Author // 著者構造体をネスト
    Pages  int
}

func main() {
    book := Book{
        Title: "Go言語プログラミング",
        Author: Author{
            Name: "Alan Donovan & Brian Kernighan",
            Bio:  "Go言語の古典的な決定版。",
        },
        Pages: 384,
    }
    fmt.Println("書名:", book.Title)           // 出力: 書名: Go言語プログラミング
    fmt.Println("著者:", book.Author.Name)     // 出力: 著者: Alan Donovan & Brian Kernighan
}