【初心者向け】Go言語の構造体をマスターしよう!定義・初期化・埋め込みまで徹底解説

Go

Go言語(Golang)でプログラムを書いていると、複数のデータをひとまとめにしたい場面がよくあります。

そんなときに便利なのが「構造体(struct)」です。

この記事では、Go言語の構造体について、基本の使い方から、埋め込み(Go独自の機能)やメソッドとの連携まで、ステップバイステップで説明します。

スポンサーリンク

構造体って何?基本の考え方

構造体(struct)とは

構造体の基本
構造体は、複数の違う型の値をまとめて扱うための型です。C言語の構造体と似ており、Goではオブジェクト指向の代わりによく使われます。

身近な例で考えてみよう
人の情報を管理することを考えてみましょう。

  • 名前:文字列
  • 年齢:数値
  • メールアドレス:文字列

これらの情報をバラバラに管理するより、ひとまとめにできたら便利ですよね。

基本的な定義方法

続きの漢字かな混じり文を以下に提示します。


簡単な使い方

構造体の作成と使い方

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    // 構造体の作成
    p := Person{Name: "太郎", Age: 25}
    
    // フィールドにアクセス
    fmt.Println(p.Name) // 太郎
    fmt.Println(p.Age)  // 25
}

他の言語との比較

JavaやPythonのクラスとの違い

// Java
class Person {
    String name;
    int age;
}
# Python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Goの構造体の特徴

  • シンプルな文法
  • クラスのような複雑さがない
  • でも必要な機能はそろっている

構造体は「名前付きの箱」のようなもので、複数の値を整理して保持できます。

次は、構造体の初期化方法を詳しく見ていきましょう。


構造体の初期化と値の取り出し

いろいろな初期化方法

方法1:フィールド指定で初期化(推奨)

p := Person{Name: "花子", Age: 30}

方法2:部分的な初期化

p := Person{Name: "次郎"} // Ageは0(デフォルト値)になる

方法3:空の構造体を作って後から設定

var p Person
p.Name = "三郎"
p.Age = 22

方法4:順番指定で初期化(非推奨)

p := Person{"四郎", 35} // フィールドの順番で指定

なぜ順番指定は非推奨?

  • フィールドの順番が変わるとバグの原因になる
  • コードが読みづらい
  • 保守性が悪い

デフォルト値について

Goの型別デフォルト値

  • string:空文字列 ""
  • int:0
  • bool:false
  • ポインタ:nil

type Product struct {
    Name  string
    Price int
    Stock bool
}

var p Product
fmt.Printf("%+v\n", p) // {Name: Price:0 Stock:false}

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

値の取り出し

fmt.Println(p.Name)  // 値を読み取り
fmt.Println(p.Age)   // 値を読み取り

値の変更

p.Age = 26           // 値を変更
p.Name = "新しい名前" // 値を変更

構造体全体の表示

fmt.Printf("%+v\n", p) // フィールド名も表示
fmt.Println(p)         // 値のみ表示

実用的な例

ユーザー情報の管理

type User struct {
    ID       int
    Username string
    Email    string
    Active   bool
}

func main() {
    // 新しいユーザーを作成
    user := User{
        ID:       1,
        Username: "yamada",
        Email:    "yamada@example.com",
        Active:   true,
    }
    
    // ユーザー情報を表示
    fmt.Printf("ユーザーID: %d\n", user.ID)
    fmt.Printf("ユーザー名: %s\n", user.Username)
    
    // アクティブ状態を変更
    user.Active = false
    fmt.Printf("アクティブ: %t\n", user.Active)
}


構造体とメソッドの連携

メソッドって何?

メソッドの基本
メソッドは、特定の型に関連づけられた関数です。その型の値に対して操作を行うことができます。

他の言語との比較

// Java
class Person {
    String name;
    void greet() {
        System.out.println("こんにちは、" + name + "です");
    }
}
// Go
type Person struct {
    Name string
}

func (p Person) Greet() {
    fmt.Printf("こんにちは、%sです\n", p.Name)
}

基本的なメソッドの定義

値レシーバーのメソッド

type Person struct {
    Name string
    Age  int
}

func (p Person) Greet() {
    fmt.Printf("こんにちは、%sです。%d歳です。\n", p.Name, p.Age)
}

func (p Person) IsAdult() bool {
    return p.Age >= 20
}

使い方

p := Person{Name: "田中", Age: 25}
p.Greet()                     // こんにちは、田中です。25歳です。
fmt.Println(p.IsAdult())     // true

ポインタレシーバーで値を変更

値レシーバーの問題点

func (p Person) Birthday() {
    p.Age++ // これは元の構造体を変更しない!
}

p := Person{Name: "佐藤", Age: 30}
p.Birthday()
fmt.Println(p.Age) // まだ30のまま

ポインタレシーバーでの解決

func (p *Person) Birthday() {
    p.Age++ // ポインタなので元の構造体を変更する
}

p := Person{Name: "佐藤", Age: 30}
p.Birthday()
fmt.Println(p.Age) // 31に変更される

いつポインタレシーバーを使う?

ポインタレシーバーを使う場面

  1. 構造体の値を変更したいとき
  2. 構造体が大きくて、コピーのコストを避けたいとき
  3. 一貫性を保ちたいとき(すべてのメソッドで同じレシーバー型)

値レシーバーを使う場面

  1. 値を変更しないとき
  2. 構造体が小さいとき
  3. イミュータブル(不変)な設計にしたいとき

実用的なメソッドの例

銀行口座の例

type BankAccount struct {
    owner   string
    balance int
}

// 残高を確認(値レシーバー)
func (a BankAccount) GetBalance() int {
    return a.balance
}

// 入金(ポインタレシーバー)
func (a *BankAccount) Deposit(amount int) {
    if amount > 0 {
        a.balance += amount
    }
}

// 出金(ポインタレシーバー)
func (a *BankAccount) Withdraw(amount int) bool {
    if amount > 0 && a.balance >= amount {
        a.balance -= amount
        return true
    }
    return false
}

// 口座情報を表示(値レシーバー)
func (a BankAccount) String() string {
    return fmt.Sprintf("口座名義: %s, 残高: %d円", a.owner, a.balance)
}

使い方

account := BankAccount{owner: "山田太郎", balance: 10000}

fmt.Println(account.String())        // 口座名義: 山田太郎, 残高: 10000円
account.Deposit(5000)                // 5000円入金
fmt.Println(account.GetBalance())    // 15000

if account.Withdraw(3000) {          // 3000円出金
    fmt.Println("出金成功")
}
fmt.Println(account.String())        // 口座名義: 山田太郎, 残高: 12000円


埋め込み構造体と継承風の使い方

Goには継承がない!

他の言語の継承

// Java
class Animal {
    String name;
}

class Dog extends Animal {
    String breed;
}

Goのアプローチ
Goには継承がありませんが、「構造体の埋め込み」により、似たようなことが実現できます。これを**コンポジション(構成)**といいます。

基本的な埋め込み

基本構造体の定義

type Person struct {
    Name string
    Age  int
}

type Address struct {
    City    string
    Country string
}

埋め込み構造体の定義

type Employee struct {
    Person          // 埋め込み(フィールド名がない)
    Address         // 埋め込み
    ID      int
    Salary  int
}

埋め込みの使い方

初期化

e := Employee{
    Person:  Person{Name: "佐藤恵子", Age: 35},
    Address: Address{City: "大阪", Country: "日本"},
    ID:      1001,
    Salary:  500000,
}

フィールドへのアクセス

// 直接アクセス(埋め込みの恩恵)
fmt.Println(e.Name)    // 佐藤恵子
fmt.Println(e.City)    // 大阪

// 明示的なアクセス
fmt.Println(e.Person.Name)   // 佐藤恵子
fmt.Println(e.Address.City)  // 大阪

メソッドの継承風動作

基本構造体にメソッドを定義

func (p Person) Greet() {
    fmt.Printf("こんにちは、%sです\n", p.Name)
}

func (p Person) GetAge() int {
    return p.Age
}

埋め込んだ構造体でもメソッドが使える

e := Employee{
    Person: Person{Name: "田中", Age: 28},
    ID:     2001,
}

e.Greet()               // こんにちは、田中です
fmt.Println(e.GetAge()) // 28

メソッドのオーバーライド

独自のメソッドを定義

func (e Employee) Greet() {
    fmt.Printf("こんにちは、社員番号%dの%sです\n", e.ID, e.Name)
}

func (e Employee) GetInfo() string {
    return fmt.Sprintf("ID: %d, 名前: %s, 給与: %d円", e.ID, e.Name, e.Salary)
}

使い方

e := Employee{
    Person: Person{Name: "山田", Age: 30},
    ID:     3001,
    Salary: 600000,
}

e.Greet()                // こんにちは、社員番号3001の山田です
fmt.Println(e.GetAge())  // 30(Personのメソッド)
fmt.Println(e.GetInfo()) // ID: 3001, 名前: 山田, 給与: 600000円

複数の埋め込み

複数の構造体を埋め込む

type Contact struct {
    Email string
    Phone string
}

type Manager struct {
    Employee        // 社員情報
    Contact         // 連絡先情報
    TeamSize int    // チームサイズ
}

使い方

m := Manager{
    Employee: Employee{
        Person: Person{Name: "鈴木", Age: 40},
        ID:     4001,
        Salary: 800000,
    },
    Contact: Contact{
        Email: "suzuki@company.com",
        Phone: "090-1234-5678",
    },
    TeamSize: 5,
}

fmt.Println(m.Name)      // 鈴木
fmt.Println(m.Email)     // suzuki@company.com
fmt.Println(m.TeamSize)  // 5

名前の衝突を避ける

問題のある例

type A struct {
    Name string
}

type B struct {
    Name string
}

type C struct {
    A
    B
}

// c.Name とアクセスできない(どちらのNameか不明)

解決方法

c := C{
    A: A{Name: "AのName"},
    B: B{Name: "BのName"},
}

fmt.Println(c.A.Name) // AのName
fmt.Println(c.B.Name) // BのName

構造体の埋め込みは、Goの設計哲学「構成による再利用」を体現するものです。
次は「構造体タグとJSONとの連携」について紹介します。


構造体タグとJSONとの連携

構造体タグって何?

タグの基本
構造体タグは、フィールドにメタデータを付加するためのしくみです。
主にシリアライゼーション(データの変換)で使われます。

基本的な書き方

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

JSONとの連携

JSON変換の例

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email"`
}

func main() {
    // 構造体からJSONへ
    u := User{Name: "直樹", Age: 28, Email: "naoki@example.com"}
    jsonData, err := json.Marshal(u)
    if err != nil {
        fmt.Println("エラー:", err)
        return
    }
    fmt.Println(string(jsonData)) // {"name":"直樹","age":28,"email":"naoki@example.com"}

    // JSONから構造体へ
    jsonStr := `{"name":"美咲","age":25,"email":"misaki@example.com"}`
    var user User
    err = json.Unmarshal([]byte(jsonStr), &user)
    if err != nil {
        fmt.Println("エラー:", err)
        return
    }
    fmt.Printf("%+v\n", user) // {Name:美咲 Age:25 Email:misaki@example.com}
}

いろいろなタグオプション

フィールドを無視

type User struct {
    Name     string `json:"name"`
    Password string `json:"-"`        // JSONに含めない
    Age      int    `json:"age"`
}

空の値を省略

type User struct {
    Name  string `json:"name"`
    Age   int    `json:"age,omitempty"`   // 0の場合は省略
    Email string `json:"email,omitempty"` // 空文字の場合は省略
}

フィールド名を変更

type User struct {
    ID       int    `json:"user_id"`
    FullName string `json:"full_name"`
    Age      int    `json:"age"`
}

API開発での活用

レスポンス用の構造体

type APIResponse struct {
    Status  string      `json:"status"`
    Message string      `json:"message,omitempty"`
    Data    interface{} `json:"data,omitempty"`
}

type UserProfile struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
    Created  string `json:"created_at"`
}

使い方

user := UserProfile{
    ID:       1,
    Username: "yamada",
    Email:    "yamada@example.com",
    Created:  "2024-01-01T00:00:00Z",
}

response := APIResponse{
    Status: "success",
    Data:   user,
}

jsonData, _ := json.Marshal(response)
fmt.Println(string(jsonData))
// {"status":"success","data":{"id":1,"username":"yamada","email":"yamada@example.com","created_at":"2024-01-01T00:00:00Z"}}

以下は「他の形式との連携」セクションの漢字かな混じり文です。


他の形式との連携

YAMLとの連携

type Config struct {
    Host string `yaml:"host"`
    Port int    `yaml:"port"`
    DB   struct {
        Name string `yaml:"name"`
        User string `yaml:"user"`
    } `yaml:"database"`
}

YAMLでは、構造体タグに yaml:"..." を使うことで、設定ファイルの読み書きが可能になります。
たとえば、以下のようなYAMLファイルと対応できます:

host: localhost
port: 5432
database:
  name: mydb
  user: admin

XMLとの連携

type Book struct {
    Title  string `xml:"title"`
    Author string `xml:"author"`
    ISBN   string `xml:"isbn,attr"` // 属性として出力
}

XMLでは xml:"フィールド名" というタグを使い、
attr オプションで属性として出力することもできます。

データベースとの連携(GORM)

type User struct {
    ID       uint   `gorm:"primaryKey"`
    Name     string `gorm:"size:100;not null"`
    Email    string `gorm:"uniqueIndex"`
    Age      int    `gorm:"default:0"`
}

GORMなどのORMライブラリでは、タグを使ってフィールドの制約やデータベースの仕様を定義できます:

  • primaryKey: 主キー
  • size: カラムのサイズ
  • not null: NULLを許容しない
  • uniqueIndex: 一意制約
  • default: デフォルト値

実際のWebアプリケーションでの例

ユーザー登録APIの例

type RegisterRequest struct {
    Username string `json:"username" validate:"required,min=3"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

type RegisterResponse struct {
    UserID  int    `json:"user_id"`
    Message string `json:"message"`
}

ハンドラー関数

func registerHandler(w http.ResponseWriter, r *http.Request) {
    var req RegisterRequest

    // JSONをパース
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    // ユーザー作成処理(例)
    userID := createUser(req.Username, req.Email, req.Password)

    // レスポンスを返す
    resp := RegisterResponse{
        UserID:  userID,
        Message: "User created successfully",
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

このように、構造体タグはAPI通信・ファイル保存・DB連携など、実践的な開発で大きな役割を果たします。


より高度な使い方

インターフェースとの組み合わせ

インターフェースの定義

type Speaker interface {
    Speak() string
}

type Walker interface {
    Walk() string
}

type Animal interface {
    Speaker
    Walker
}

Goでは、インターフェースを使うことで、構造体に共通のふるまい(関数の集合)を持たせることができます。
上記の例では、Animal インターフェースが SpeakerWalker を内包しており、複数のふるまいをまとめた抽象型として機能します。

構造体での実装

type Dog struct {
    Name  string
    Breed string
}

func (d Dog) Speak() string {
    return fmt.Sprintf("%sがワンワンと鳴いています", d.Name)
}

func (d Dog) Walk() string {
    return fmt.Sprintf("%sが散歩しています", d.Name)
}

type Cat struct {
    Name  string
    Color string
}

func (c Cat) Speak() string {
    return fmt.Sprintf("%sがニャーニャーと鳴いています", c.Name)
}

func (c Cat) Walk() string {
    return fmt.Sprintf("%sがそっと歩いています", c.Name)
}

使い方

func makeAnimalSpeak(a Animal) {
    fmt.Println(a.Speak())
    fmt.Println(a.Walk())
}

dog := Dog{Name: "ポチ", Breed: "柴犬"}
cat := Cat{Name: "たま", Color: "白"}

makeAnimalSpeak(dog)
// → ポチがワンワンと鳴いています
// → ポチが散歩しています

makeAnimalSpeak(cat)
// → たまがニャーニャーと鳴いています
// → たまがそっと歩いています

このように、インターフェースを使うと型に依存しない汎用的な関数を定義できます。


ファクトリーパターン

コンストラクタ関数

type User struct {
    id       int
    username string
    email    string
    created  time.Time
}

func NewUser(username, email string) *User {
    return &User{
        id:       generateID(),
        username: username,
        email:    email,
        created:  time.Now(),
    }
}

func (u *User) GetUsername() string {
    return u.username
}

func (u *User) GetEmail() string {
    return u.email
}

使い方

user := NewUser("yamada", "yamada@example.com")
fmt.Println(user.GetUsername()) // yamada

このような「New◯◯関数」を使うことで、構造体の生成処理を共通化し、安全な初期化・一貫性ある管理が可能になります。
とくに、ID生成・時刻の自動設定など、毎回必要な処理をカプセル化できるのが利点です。

以下は最終セクション「まとめ:構造体でGoプログラミングをマスター」の漢字かな混じり文です。


まとめ:構造体でGoプログラミングをマスター

Go言語における構造体は、オブジェクトのようにデータをまとめて扱える便利な機能です。

この記事のポイント

  • 定義・初期化:フィールド名を明示して安全に初期化
  • メソッドの追加:値レシーバーとポインタレシーバーの使い分け
  • 構造体の埋め込み:継承の代わりにコンポジションを使う
  • タグと外部連携:JSONなどとのスムーズなデータ交換

コメント

タイトルとURLをコピーしました