Go言語のstructで構造体を定義する方法をわかりやすく解説

スポンサーリンク
スポンサーリンク
amazon
スマイルSALE
--:--:--
~
ad. 価格範囲を指定して商品を探せます

structの概要

複数の異なる型のデータをひとまとめにするGoの予約語

struct

概要 struct(構造体)は、任意の異なるデータ型(プリミティブ型、配列、スライス、マップ、あるいは他の構造体)のフィールドを1つのまとまりとしてカプセル化し、新しいカスタムデータ型を定義するための複合データ型キーワードです。Go言語には「クラス(class)」が存在しないため、データ構造を表現する中核の仕組みとして機能します。

  • 密接に関連する複数のデータを1つのオブジェクトとして抽象化・モジュール化できる。
  • 関数を「レシーバ」としてバインド(結合)することで、構造体に対するメソッドを定義し、オブジェクト指向ライクなカプセル化を実現する。
  • ポインタを適用することで、大容量データであってもメモリコピーのオーバーヘッドを発生させずに高速かつ効率的な参照渡し制御が行える。

structのオブジェクト指向的表現をイメージで理解

基本的なstructの使い方

以下のコードは、structを使って人物の情報を管理する例です。

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)
}

解説:

  • type Person struct という宣言により、新しい独自の型 Person を定義しています。
  • インスタンス(構造体実体)を生成する際は、型名{フィールド: 値, ...} のリテラル記法を用います。フィールド名を明示的に記述することで、初期化の順番に依存しない安全なインスタンス化が可能になります。
  • 生成したインスタンスのフィールドへの読み書きは、ドット演算子(p.Name)を用いて直接アクセスします。

実行結果:

名前: 太郎
年齢: 25

ポインタを使った構造体の参照

Goの構造体は標準では「値型」として振る舞いますが、ポインタ演算子(*)やアドレス参照演算子(&)を付与することで、メモリ上の同一実体を効率的に共有・操作できます。

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func changeAge(p *Person, newAge int) {
    p.Age = newAge // ポインタを介して元の実体のフィールドを更新
}

func main() {
    p := Person{Name: "花子", Age: 30}

    fmt.Println("変更前の年齢:", p.Age)

    changeAge(&p, 35) // インスタンスのアドレス(ポインタ)を渡す
    fmt.Println("変更後の年齢:", p.Age)
}

解説:

  • 関数 changeAge の第1引数の型を *Person とすることで、構造体そのもののコピーではなく「構造体が格納されているメモリ上の位置情報(アドレス)」を受け取ります。
  • Go言語では、ポインタ経由のフィールドアクセスであっても、C言語のようなアロー演算子(->)を使わず、p.Age のように 自動でデリファレンス(間接参照)されて直接ドット演算子で記述できる という親切な糖衣構文(シンタックスシュガー)が採用されています。

実行結果:

変更前の年齢: 30
変更後の年齢: 35

構造体にメソッドを定義

構造体定義の外部で特定の関数に「レシーバ」を付与することで、その型専用のメソッド(オブジェクトの持つ関数)を拡張できます。

package main

import "fmt"

// Person構造体の定義
type Person struct {
    Name string
    Age  int
}

// メソッドの定義(ポインタレシーバ)
func (p *Person) Greet() {
    fmt.Printf("こんにちは、私は%sです。%d歳です。\n", p.Name, p.Age)
}

func main() {
    p := Person{Name: "次郎", Age: 20}
    p.Greet() // 構造体のインスタンスから直接メソッドを呼び出し
}

解説:

  • func (p *Person) Greet() の (p *Person) の部分を ポインタレシーバ と呼びます。メソッド内で構造体の状態を更新したい場合や、メソッド呼び出しのたびに発生する構造体の値コピーのオーバーヘッドを削減したい場合にポインタレシーバを選択するのがGoの鉄則です。

実行結果:

こんにちは、私は次郎です。20歳です。

構造体の埋め込み(コンポジションによる機能拡張)

Go言語はオブジェクト指向の「クラスの継承(Inheritance)」をあえて廃止していますが、その代わりに 構造体の埋め込み(Embedding / 構造体のコンポジション) という仕組みにより、既存の構造体の能力を包括・拡張することができます。

package main

import "fmt"

// 基本構造体
type Person struct {
    Name string
    Age  int
}

// Personを埋め込む新しい構造体
type Employee struct {
    Person // フィールド名を省略して型名のみを記述(型埋め込み)
    Position string
}

func main() {
    e := Employee{
        Person:   Person{Name: "太郎", Age: 40},
        Position: "エンジニア",
    }

    // 埋め込まれた側のフィールドに直接アクセス(フィールドの昇格)
    fmt.Println("名前:", e.Name)       
    fmt.Println("年齢:", e.Age)
    fmt.Println("職業:", e.Position)
}

解説:

  • Employee 構造体の中に、変数名を付けずに Person 型名のみを記述することで「型埋め込み(Anonymous Fields)」が成立します。
  • これにより、埋め込まれた Person の持つフィールドやメソッドが、さも Employee 自身のものであるかのようにドット演算子で直接アクセス可能になる フィールドの昇格(Promoted Fields) が発生します(例:e.Person.Name だけでなく e.Name と書ける)。

実行結果:

名前: 太郎
年齢: 40
職業: エンジニア

学術的・設計的注意点

  • 値代入における暗黙のシャローコピーの罠: 構造体はデフォルトで「値型」であるため、p2 := p1 のように別の変数に代入したり、関数の引数にポインタを使わずそのまま渡したりすると、メモリ上で 全フィールドの値が複製(シャドーコピー) されます。構造体のサイズが大きい場合、この複製処理はCPUやメモリ帯域に負荷をかけるため、関数の引数やメソッドレシーバでは特別な理由がない限りポインタ(*struct)を使用することが強く推奨されます。
  • 可視性とパッケージ間エクスポートの厳格ルール: struct 自体の名前、および struct 内部の各フィールド名は、先頭の1文字が大文字か小文字かによってパッケージ外への公開範囲が厳格に決定 されます。先頭が大文字のフィールド(例:Name)は外部パッケージから自由に読み書きできますが、小文字のフィールド(例:age)は、たとえ構造体自体が外部にエクスポートされていても、他パッケージのコードからは一切見えず、アクセスを試みるとコンパイルエラー(unexported field)になります。
  • 構造体の比較可能性(Comparability)の制限: Go言語では、すべての構造体が == や != で直接比較できるわけではありません。構造体を構成するすべてのフィールドの型が「比較可能(数値、文字列、ポインタ、配列など)」である場合のみ、構造体同士を直接比較できます。もしフィールドに スライス、マップ、関数などの比較不可能な型(nilとしか比較できない型)が1つでも含まれている場合、その構造体全体が比較不可能 となり、コンパイルエラーを引き起こします。

よくある質問

Q: 構造体のインスタンスを生成したとき、初期化を忘れたフィールドの値はどうなりますか?
A: Go言語には未定義のメモリ破壊状態(ガベージ値)は存在しません。初期化リテラルで省略された、あるいは単に var p Person と宣言された構造体の各フィールドは、それぞれの型の ゼロ値(数値なら 0、文字列なら ""、ポインタやスライスなら nil)で安全かつ確実に初期化されます。
Q: ポインタレシーバ(*T)と値レシーバ(T)の使い分けの明確な基準は?
A: 主な基準は2点です。(1) メソッド内部で構造体の状態(フィールド)を書き換えたい場合 は、必ずポインタレシーバを選択します。(2) 構造体のサイズが大きく、値コピーによるオーバーヘッドを避けたい場合もポインタにします。逆に、一度生成したら状態が変わらない極めて小さな構造体(例:座標を表す Point{X, Y int} など)の場合に限り、値レシーバが選択されることがあります。
Q: 構造体のフィールドの後ろについている `json:"name"` のような文字列は何ですか?
A: それは 構造体タグ(Struct Tag) と呼ばれるメタデータ記述子です。リフレクション(`reflect` パッケージ)を介してプログラム実行時に読み取ることができ、主に json.Marshal やデータベースのO/Rマッパーなどの外部ライブラリが、構造体のフィールドと外部データ形式(JSONキーやDBカラム名)をマッピングする際のアノテーションとして広く重用されています。
Q: 構造体の埋め込み(コンポジション)において、親と子で同名のフィールドが競合した場合はどうなりますか?
A: Employee に Name フィールドがあり、埋め込まれた Person にも Name があった場合、e.Name と記述すると 最外殻のEmployee側のフィールドが最優先(シャドーイング) されます。埋め込まれた側のフィールドが消滅するわけではないため、e.Person.Name とフルパスで明示的に指定すれば競合を回避してアクセス可能です。

まとめ

  • structは複数の異なるデータ型を1つにまとめ、クラスに代わるカスタムデータ型を定義する予約語。
  • 関数にレシーバを付与してメソッド化することで、データと振る舞いをカプセル化したクリーンな設計が可能。
  • 構造体のコピーを防ぎ、関数内での破壊的更新を許可するには ポインタ(*struct) を適用する。
  • オブジェクト指向の継承の代わりに 型埋め込み(コンポジション) を採用し、フィールドの昇格による柔軟な構造拡張を実現。
  • 外部パッケージからのアクセス制御は、フィールド名の 先頭1文字が大文字(公開)か小文字(非公開)か の規則で完全制御。