構造体

構造体

記入用紙を想像してください。氏名、年齢、電話番号、住所 — これらの情報はお互いに関連しており、まとめて一人の人間を表します。Goでは、struct がその「記入用紙」にあたります。複数の異なる型のフィールドを組み合わせて、カスタムの複合型を作成します。マップと比較すると、構造体のフィールドはコンパイル時に決定されるため、型安全で意味が明確です。構造体はGoにおけるデータ整理の中心的な方法です。


1. コアコンセプト

コンセプト 説明
定義 type StructName struct { ... }、フィールド名は型の前に記述
初期化 リテラル S{f1: v1, f2: v2}new(S) はポインタを返す、&S{}
フィールドアクセス ドット記法 s.Field、ポインタでもドット記法 p.Field-> は不要)
値型 構造体は値型。代入や関数引数で構造体全体がコピーされる
ポインタレシーバ &s でポインタを取得。ポインタ経由で構造体を変更すると元の値に影響する
無名フィールド フィールド名なしで型のみ記述。型名がフィールド名となり、「合成」を実現
ネストされた構造体 構造体のフィールド自体が構造体になることができ、多階層のネストをサポート
構造体タグ バッククォートで囲まれたメタデータ。JSON/データベースのマッピングによく使用
⚠️ 注意: 構造体は値型です。引数として渡すとコピーが作成されます。元の値を変更するには、ポインタ *S を渡してください。


2. 基本構文/使い方

構造体の定義

GO
// Person 構造体を定義
type Person struct {
    Name string
    Age  int
    City string
}
💡 ヒント: Goでは、構造体のフィールド名が大文字で始まる場合、エクスポート(公開)されます。小文字で始まる場合はアンエクスポート(非公開)となり、パッケージ内からのみアクセスできます。

初期化方法

GO
// 方法1: フィールド名: 値(推奨、可読性が高く、順序は問わない)
p1 := Person{Name: "Alice", Age: 25, City: "Beijing"}

// 方法2: 順序で代入(フィールドが多い場合は非推奨、エラーが起きやすい)
p2 := Person{"Bob", 30, "Shanghai"}

// 方法3: 先に宣言してから代入
var p3 Person
p3.Name = "Charlie"
p3.Age = 28
p3.City = "Guangzhou"

// 方法4: new で作成(ポインタを返す)
p4 := new(Person)  // *Person
p4.Name = "Diana"

// 方法5: アドレスを取得(ポインタを返す)
p5 := &Person{Name: "Eve", Age: 22, City: "Shenzhen"}
💡 ヒント: var p Person で宣言された構造体は、すべてのフィールドがそれぞれの型のゼロ値で初期化されます(string → ""、int → 0、bool → false)。

フィールドへのアクセスと変更

GO
p := Person{Name: "Alice", Age: 25}

// フィールドの読み取り
fmt.Println(p.Name)  // Alice

// フィールドの変更
p.Age = 26
fmt.Println(p.Age)   // 26
💡 ヒント: ポインタと値の両方でフィールドにアクセスする構文は同じです。Goが自動的にデリファレンスを行うため、p->Name は不要です。

構造体ポインタ

GO
p := &Person{Name: "Alice", Age: 25}

// Goが自動的にデリファレンスするため、直接ドット記法を使用
fmt.Println(p.Name)  // Alice

// ポインタ経由で元の値を変更
p.Age = 30
💡 ヒント: Goには -> 演算子がありません。p が値であってもポインタであって、常に p.Field でフィールドにアクセスします。


3. サンプルコード

例:基本的な使い方(難易度⭐)

Book 構造体を定義し、インスタンスを作成して情報を表示します。

GO
package main

import "fmt"

// Book は書籍の構造体を定義
type Book struct {
    Title  string
    Author string
    Pages  int
    Price  float64
}

func main() {
    // リテラルで作成
    book1 := Book{
        Title:  "The Go Programming Language",
        Author: "Alan Donovan",
        Pages:  350,
        Price:  59.9,
    }

    // 構造体を表示
    fmt.Println(book1)

    // 個別のフィールドにアクセス
    fmt.Printf("Title: %s\n", book1.Title)
    fmt.Printf("Author: %s\n", book1.Author)
    fmt.Printf("Price: %.1f\n", book1.Price)

    // フィールドを変更
    book1.Price = 49.9
    fmt.Printf("New price: %.1f\n", book1.Price)

    // 順序で作成(非推奨)
    book2 := Book{"Python Crash Course", "Eric Matthes", 280, 45.0}
    fmt.Println(book2)
}
▶ 試してみよう

出力:

TEXT
{The Go Programming Language Alan Donovan 350 59.9}
Title: The Go Programming Language
Author: Alan Donovan
Price: 59.9
New price: 49.9
{Python Crash Course Eric Matthes 280 45}

例:応用的な使い方(難易度⭐⭐)

ポインタ、ネストされた構造体、無名フィールドの使用例です。

GO
package main

import "fmt"

// Address 構造体
type Address struct {
    City    string
    Street  string
    ZipCode string
}

// Employee 構造体(無名フィールド Address を使用)
type Employee struct {
    Name    string
    Age     int
    Address        // 無名フィールド:型名がフィールド名になる
    Salary  float64
}

func main() {
    // ========== ネストされた構造体の初期化 ==========
    emp := Employee{
        Name:   "Alice",
        Age:    30,
        Salary: 15000,
        Address: Address{
            City:    "Beijing",
            Street:  "1 Zhongguancun Street",
            ZipCode: "100080",
        },
    }

    fmt.Printf("Employee: %s\n", emp.Name)
    fmt.Printf("City: %s\n", emp.Address.City)  // 明示的なアクセス

    // ========== 無名フィールドの「プロモーション」アクセス ==========
    // 無名フィールドは型名で直接アクセスできる
    fmt.Printf("Street: %s\n", emp.Street)   // emp.Address.Street と同じ
    fmt.Printf("ZipCode: %s\n", emp.ZipCode) // emp.Address.ZipCode と同じ

    // ========== ポインタ操作 ==========
    empPtr := &emp

    // ポインタ経由で変更(Goが自動的にデリファレンス)
    empPtr.Age = 31
    empPtr.Salary = 18000
    empPtr.City = "Shanghai" // ポインタ経由で無名フィールドを変更

    fmt.Printf("\n変更後: %+v\n", *empPtr)

    // ========== 値型とポインタ型 ==========
    fmt.Println("\n--- 値型とポインタ型 ---")

    // 値型:代入はコピー
    emp2 := emp
    emp2.Name = "Bob"
    fmt.Printf("emp.Name:  %s\n", emp.Name)  // まだ Alice
    fmt.Printf("emp2.Name: %s\n", emp2.Name) // Bob

    // ポインタ型:代入は共有
    emp3 := empPtr
    emp3.Name = "Charlie"
    fmt.Printf("empPtr.Name: %s\n", empPtr.Name) // Charlie(変更された)
}
▶ 試してみよう

出力:

TEXT
Employee: Alice
City: Beijing
Street: 1 Zhongguancun Street
ZipCode: 100080

変更後: {Name:Alice Age:31 Address:{City:Shanghai Street:1 Zhongguancun Street ZipCode:100080} Salary:18000}

--- 値型とポインタ型 ---
emp.Name:  Alice
emp2.Name: Bob
empPtr.Name: Charlie

例:総合的な応用(難易度⭐⭐⭐)

構造体タグを使ったJSONシリアライズ/デシリアライズと、学生管理システムの構築を行います。

GO
package main

import (
    "encoding/json"
    "fmt"
    "sort"
    "strings"
)

// Student 構造体(JSONタグ付き)
type Student struct {
    ID     int      `json:"id"`
    Name   string   `json:"name"`
    Age    int      `json:"age"`
    Score  float64  `json:"score"`
    Tags   []string `json:"tags,omitempty"` // omitempty: 空の場合は省略
    secret string   // アンエクスポートフィールド、JSONからはアクセス不可
}

// ClassRoom(ネストされた構造体)
type ClassRoom struct {
    ClassName string    `json:"class_name"`
    Students  []Student `json:"students"`
}

// AddStudent は学生を追加する
func (c *ClassRoom) AddStudent(s Student) {
    c.Students = append(c.Students, s)
}

// GetTopN は成績上位N名の学生を取得する
func (c *ClassRoom) GetTopN(n int) []Student {
    // 元のデータを変更しないようコピーを作成
    sorted := make([]Student, len(c.Students))
    copy(sorted, c.Students)

    // 成績の降順でソート
    sort.Slice(sorted, func(i, j int) bool {
        return sorted[i].Score > sorted[j].Score
    })

    if n > len(sorted) {
        n = len(sorted)
    }
    return sorted[:n]
}

// Average は平均点を計算する
func (c *ClassRoom) Average() float64 {
    if len(c.Students) == 0 {
        return 0
    }
    total := 0.0
    for _, s := range c.Students {
        total += s.Score
    }
    return total / float64(len(c.Students))
}

// SearchByName は名前で検索する(あいまい一致)
func (c *ClassRoom) SearchByName(keyword string) []Student {
    var result []Student
    for _, s := range c.Students {
        if strings.Contains(s.Name, keyword) {
            result = append(result, s)
        }
    }
    return result
}

func main() {
    // クラスを作成
    class := &ClassRoom{ClassName: "2024年度1組"}

    // 学生を追加
    students := []Student{
        {ID: 1, Name: "Zhang San", Age: 18, Score: 92.5, Tags: []string{"数学が得意", "学級委員"}},
        {ID: 2, Name: "Li Si", Age: 19, Score: 88.0, Tags: []string{"スポーツが得意"}},
        {ID: 3, Name: "Wang Wu", Age: 18, Score: 95.5},
        {ID: 4, Name: "Zhao Liu", Age: 20, Score: 78.0, Tags: []string{"芸術の才能"}},
        {ID: 5, Name: "Zhang Wei", Age: 19, Score: 91.0, Tags: []string{"数学が得意", "コンテスト受賞"}},
    }

    for _, s := range students {
        class.AddStudent(s)
    }

    // ========== JSONシリアライズ ==========
    fmt.Println("=== JSONシリアライズ ===")
    jsonData, err := json.MarshalIndent(class, "", "  ")
    if err != nil {
        fmt.Println("シリアライズに失敗しました:", err)
    } else {
        fmt.Println(string(jsonData))
    }

    // ========== JSONデシリアライズ ==========
    fmt.Println("\n=== JSONデシリアライズ ===")
    jsonStr := `{"id":6,"name":"Sun Qi","age":17,"score":97.0,"tags":["英語が得意"]}`
    var newStudent Student
    if err := json.Unmarshal([]byte(jsonStr), &newStudent); err != nil {
        fmt.Println("デシリアライズに失敗しました:", err)
    } else {
        fmt.Printf("新しい学生: %+v\n", newStudent)
    }

    // ========== 機能デモ ==========
    fmt.Println("\n=== クラス情報 ===")
    fmt.Printf("クラス: %s、合計%d名\n", class.ClassName, len(class.Students))
    fmt.Printf("平均点: %.1f\n", class.Average())

    fmt.Println("\n=== 成績上位3名 ===")
    top3 := class.GetTopN(3)
    for i, s := range top3 {
        fmt.Printf("  #%d: %s (%.1f)\n", i+1, s.Name, s.Score)
    }

    fmt.Println("\n=== 「Zhang」で検索 ===")
    results := class.SearchByName("Zhang")
    for _, s := range results {
        fmt.Printf("  %s (ID: %d, 成績: %.1f)\n", s.Name, s.ID, s.Score)
    }
}
▶ 試してみよう

出力:

TEXT
=== JSONシリアライズ ===
{
  "class_name": "2024年度1組",
  "students": [
    {
      "id": 1,
      "name": "Zhang San",
      "age": 18,
      "score": 92.5,
      "tags": [
        "数学が得意",
        "学級委員"
      ]
    },
    {
      "id": 2,
      "name": "Li Si",
      "age": 19,
      "score": 88
    },
    {
      "id": 3,
      "name": "Wang Wu",
      "age": 18,
      "score": 95.5
    },
    {
      "id": 4,
      "name": "Zhao Liu",
      "age": 20,
      "score": 78,
      "tags": [
        "芸術の才能"
      ]
    },
    {
      "id": 5,
      "name": "Zhang Wei",
      "age": 19,
      "score": 91,
      "tags": [
        "数学が得意",
        "コンテスト受賞"
      ]
    }
  ]
}

=== JSONデシリアライズ ===
新しい学生: {ID:6 Name:Sun Qi Age:17 Score:97 Tags:[英語が得意]}

=== クラス情報 ===
クラス: 2024年度1組、合計5名
平均点: 89.0

=== 成績上位3名 ===
  #1: Wang Wu (95.5)
  #2: Zhang San (92.5)
  #3: Zhang Wei (91.0)

=== 「Zhang」で検索 ===
  Zhang San (ID: 1, 成績: 92.5)
  Zhang Wei (ID: 5, 成績: 91.0)

3. よくある使用場面

場面1:JSONデータのやり取り

Web開発では、構造体がJSONリクエスト/レスポンスを処理する標準的な方法です。タグがJSONフィールド名を制御します。

GO
package main

import (
    "encoding/json"
    "fmt"
)

// APIResponse は統一されたAPIレスポンス構造体
type APIResponse struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

// UserDTO はユーザーデータ転送オブジェクト
type UserDTO struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
    Password string `json:"-"` // "-" はJSONでこのフィールドを無視することを意味する
}

func main() {
    // レスポンスを構築
    user := UserDTO{
        ID:       1,
        Username: "alice",
        Email:    "alice@example.com",
        Password: "secret123", // シリアライズされない
    }

    resp := APIResponse{
        Code:    200,
        Message: "success",
        Data:    user,
    }

    // シリアライズ
    jsonBytes, _ := json.MarshalIndent(resp, "", "  ")
    fmt.Println(string(jsonBytes))

    // デシリアライズ
    jsonStr := `{"code":404,"message":"User not found"}`
    var errResp APIResponse
    json.Unmarshal([]byte(jsonStr), &errResp)
    fmt.Printf("\nエラーコード: %d、メッセージ: %s\n", errResp.Code, errResp.Message)
}

出力:

TEXT
{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "username": "alice",
    "email": "alice@example.com"
  }
}

エラーコード: 404、メッセージ: User not found

場面2:オブジェクト指向のシミュレーション(継承より合成)

Goにはクラスや継承がありません。構造体のネスト(合成)を通じてコードの再利用を実現します。

GO
package main

import "fmt"

// Logger はログ機能
type Logger struct {
    Prefix string
}

// Log はログメッセージを出力する
func (l *Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.Prefix, msg)
}

// Cache はキャッシュ機能
type Cache struct {
    data map[string]string
}

// Get はキャッシュから取得する
func (c *Cache) Get(key string) (string, bool) {
    val, ok := c.data[key]
    return val, ok
}

// Set はキャッシュに設定する
func (c *Cache) Set(key, value string) {
    if c.data == nil {
        c.data = make(map[string]string)
    }
    c.data[key] = value
}

// UserService は合成により Logger と Cache の機能を再利用する
type UserService struct {
    Logger       // Logger を埋め込み
    Cache        // Cache を埋め込み
    Name string
}

func main() {
    svc := UserService{
        Logger: Logger{Prefix: "UserService"},
        Name:   "User Service",
    }

    // 埋め込まれたメソッドを直接呼び出し
    svc.Log("サービスを開始しました")              // Logger からの継承
    svc.Set("user:1", "Alice")              // Cache からの継承
    svc.Log("キャッシュを初期化しました")

    if val, ok := svc.Get("user:1"); ok {
        svc.Log(fmt.Sprintf("ユーザーを見つけました: %s", val))
    }
}

出力:

TEXT
[UserService] サービスを開始しました
[UserService] キャッシュを初期化しました
[UserService] ユーザーを見つけました: Alice

❓ よくある質問

質問1:構造体は値型ですか?参照型ですか?

構造体は値型です。代入や関数引数で構造体全体がコピーされます。元の値を変更するには、ポインタを渡してください。

GO
func updateAge(p *Person, age int) {
    p.Age = age  // 元の値を直接変更
}

p := Person{Name: "Alice", Age: 25}
updateAge(&p, 30)
fmt.Println(p.Age)  // 30

質問2:無名フィールドとは?ネストされた構造体と何が違うの?

無名フィールドは「合成」を実装するためのものです。内側の型のメソッドとフィールドが外側の型に「プロモーション」されます。

GO
type Address struct {
    City string
}

type Person struct {
    Name string
    Address        // 無名フィールド
}

p := Person{Name: "Alice", Address: Address{City: "Beijing"}}
fmt.Println(p.City)         // 直接アクセス、p.Address.City と同じ
fmt.Println(p.Address.City) // 明示的なアクセスでも動作

ネストされた構造体に同名のフィールドがある場合は、完全パスを使用して曖昧さを解消する必要があります。

質問3:構造体タグとは?どうやって使うの?

タグはバッククォートで囲まれたメタデータで、JSON、データベースORM、その他のライブラリの動作を制御するためによく使用されます。

GO
type User struct {
    ID       int    `json:"id"              db:"user_id"`
    Name     string `json:"name"            db:"user_name"`
    Password string `json:"-"               db:"password"`    // JSONでは無視
    Email    string `json:"email,omitempty"  db:"email"`       // JSONで空の場合に省略
}

よく使われるタグ:

質問4:構造体は比較できますか?

フィールドの型に依存します。すべてのフィールドが比較可能な型(int、string、bool、配列など)であれば、==!= で比較できます。スライス、マップ、関数などの比較不可能なフィールドが含まれている場合は、直接比較できません。

GO
type Point struct { X, Y int }

p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2)  // true

// スライスを含む構造体は == を使用できない
type Data struct { Items []int }
// d1 == d2  // コンパイルエラー!

📖 まとめ


📝 演習

演習1(⭐)

WidthHeight フィールドを持つ Rectangle 構造体を定義してください。以下を実装してください:

  1. Rectangleインスタンスを作成して表示する
  2. Area() メソッドで面積を計算する
  3. Perimeter() メソッドで周囲の長さを計算する
  4. IsSquare() メソッドで正方形かどうかを判定する

演習2(⭐⭐)

Owner(string)と Balance(float64)フィールドを持つ BankAccount 構造体を定義してください。以下を実装してください:

  1. Deposit(amount) で入金
  2. Withdraw(amount) で出金(残高不足の場合は通知)
  3. Transfer(other *BankAccount, amount) で振込
  4. JSONタグを使って口座情報をシリアライズする(json:"-" で残高を隠す)

演習3(⭐⭐⭐)

商品在庫管理システムを実装してください:

  1. Product 構造体(ID、Name、Price、Stock、Tags)を定義
  2. Inventory 構造体(ShopName、Productsスライス)を定義
  3. メソッドを実装:AddProductRemoveProduct(id)FindByTag(tag)TotalValue()(在庫総額)
  4. JSONシリアライズを実装(要件:Priceは小数点以下2桁、Stockが0の場合は省略)
  5. インスタンスを作成し、商品を追加、タグで検索、総額を計算、JSONとして出力

次のレッスン

👉 08-methods - メソッド

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%