👁

Go構造体

この章では、Goの構造体について学び、ハンズオン形式で学習します。これにより、構造体でデータを束ね、メソッドで振る舞いを持たせ、埋め込みで既存の構造体を再利用する基本が身につきます。

1. 本章の概要

1.1 本章の目的

Goでは、複数の関連するフィールドを1つの型にまとめる仕組みとして 構造体 が用意されています。データを束ねるだけでなく、メソッドを付けて振る舞いを持たせたり、埋め込みで別の構造体の機能を再利用したりと、Goの設計の中心にある型です。

本章では、構造体の定義と初期化、配列やスライスでの利用、メソッドとコンストラクタ関数、合成と埋め込み、そしてタグまでを一通り扱います。

1.2 ハンズオンの流れ

小さな構造体を宣言してフィールドを扱うところから始め、メソッドを付ける、埋め込みで既存の構造体を再利用する、と段階的に手を動かします。実行結果を確認しながら書き方を身につけていきます。

1.3 事前準備

必要なツール

この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。

ツール名 関連箇所 理由
Visual Studio Code Visual Studio Codeのインストール Goのコードを記述するエディタとして使用する
Go Goのインストール 本章のサンプルコードを実行する言語処理系として使用する

2. 構造体とは

2.1 構造体が必要な理由

プログラムを書いていくと、「複数の関連するデータを1つのまとまりとして扱いたい」場面がよく出てきます。たとえば「ユーザーの名前と年齢」をひとまとめで扱いたい、といったケースです。

これまで学んだ範囲では、次のように変数を並べて書くことになります。

package main

import "fmt"

func main() {
    name := "Taro"
    age := 30

    fmt.Printf("%s (%d歳)\n", name, age)
}

1人分だけならこれでも書けます。しかし、複数人を扱いたいと考えると、次のようにフィールドごとに配列やスライスを用意することになります(以下はスライスの例です)。

package main

import "fmt"

func main() {
    names := []string{"Taro", "Jiro", "Hanako"}
    ages := []int{30, 25, 28}

    for i := 0; i < len(names); i++ {
        fmt.Printf("%s (%d歳)\n", names[i], ages[i])
    }
}

以下のような実行結果が表示されます。

Taro (30歳)
Jiro (25歳)
Hanako (28歳)

動きはしますが、この書き方には次のような問題があります。

  • 「同じインデックスが同じ人のデータを表す」というルールを、書き手が自分で維持する必要がある
  • 「住所」も扱いたくなったら、cities スライスを追加して同期を取る必要がある
  • ある人を削除するときは、すべてのスライスから同じインデックスを取り除かないと不整合になる

「1人分の情報をひとまとまりで扱いたい」というのが自然な要求ですが、配列やスライスだけではうまく表現できません。

このような場面のために、Goには構造体struct)が用意されています。関連する複数の値を1つの型にまとめて、名前を通してひとまとまりで扱う仕組みです。

2.2 構造体の定義

構造体を定義するときは、type キーワードのあとに型名を書き、struct { ... } の中にフィールドの名前と型を並べます。

📝 フィールドとは
構造体の中に含まれる個々の値のことを フィールド と呼びます。たとえば「ユーザー」を表す構造体なら、「名前」や「年齢」がそれぞれフィールドにあたります。

書き方は以下のとおりです。

type 型名 struct {
    フィールド名11
    フィールド名22
}

定義した構造体は、通常の型と同じように var で変数として宣言できます。宣言だけした場合、各フィールドはゼロ値で自動的に初期化されます。あとから 変数名.フィールド名 = 値 の形で個別のフィールドに値を代入したり、変数名.フィールド名 で参照したりできます。

📝 ゼロ値とは
Goでは、変数を宣言しただけで初期値を与えなかったとき、型ごとに決まっているゼロ値が自動的に代入されます。int なら 0float64 なら 0.0string なら ""(空文字列)、bool なら false です。「ゼロ」という名前ですが、必ずしも数値の 0 とは限らない点に注意してください。

実際に試してみましょう。次のコードは、User 型を定義し、そのインスタンスを1つ作って個別に値を設定・表示するコードです。main.go を以下の内容に書き換えて保存し、go run main.go で実行します。

📝 インスタンスとは
型に対する具体的な値のことを インスタンス と呼びます。たとえば User 型に対する「1人分のデータ」が User のインスタンスにあたります。
package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func main() {
    var u User
    u.Name = "Taro"
    u.Age = 30

    fmt.Println(u)
    fmt.Println(u.Name)
    fmt.Println(u.Age)
}

以下のような実行結果が表示されます。

{Taro 30}
Taro
30

User 全体を出力すると {Taro 30} の形で表示され、個別フィールドを出力するとそれぞれ Taro30 が得られました。

コードを解説します。

type User struct {
    Name string
    Age  int
}

type 名前 struct { ... } の形で新しい構造体の型を定義します。ここでは User という型を、Namestring)と Ageint)の2つのフィールドを持つものとして定義しています。

var u User

varUser 型の変数 uUser のインスタンス)を宣言しています。この時点では、各フィールドはゼロ値で初期化されています。

u.Name = "Taro"
u.Age = 30

変数名.フィールド名 = 値 の形で、個別のフィールドに値を代入できます。ここでは u.Name"Taro"u.Age30 を設定しています。

fmt.Println(u.Name)
fmt.Println(u.Age)

フィールドの参照もドット . を使い、u.Nameu.Age のように書きます。

3. 構造体の初期化

先ほどのように「宣言してから1つずつフィールドに代入する」書き方のほかに、宣言と同時に初期値を渡してまとめて初期化する書き方もあります。用途に応じて次の3つがあります。

3.1 すべて指定する場合

すべてのフィールドの初期値を指定する場合の書き方は以下のとおりです。

変数名 := 型名{フィールド名1: 値1, フィールド名2: 値2}

次のコードは、User のインスタンスを、宣言と同時に Name = "Taro"Age = 30 で初期化する例です(前のコードと同じ type User struct {...} を使うことを想定しています)。

u := User{Name: "Taro", Age: 30}
fmt.Println(u)

以下のような実行結果が表示されます。

{Taro 30}

宣言と初期化を1行で書けるので、コードが短くなります。実際にはこの書き方をよく使います。

3.2 何も指定しない場合

初期化子を空にすると、すべてのフィールドがゼロ値のまま作られます。書き方は以下のとおりです。

変数名 := 型名{}

次のコードは、空の初期化子で User を作る例です。

u := User{}
fmt.Println(u)

以下のような実行結果が表示されます。

{ 0}

Name""(空文字列)、Age0 になっています。var u User と同じ結果になりますが、:= で明示的に「初期化して受け取る」形にしたい場合に使えます。

3.3 一部だけ指定する場合

フィールドの一部だけを指定した場合、指定しなかった残りはゼロ値で初期化されます。書き方は以下のとおりです。

変数名 := 型名{フィールド名: 値}

次のコードは、Age を省略した User を作る例です。

u := User{Name: "Taro"}
fmt.Println(u)

以下のような実行結果が表示されます。

{Taro 0}

Age を指定していないため、int のゼロ値である 0 が自動で入っています。

3.4 配列やスライスでの利用

冒頭で示したように、複数のユーザを扱いたい場面では、フィールドごとに配列やスライスを分けるのではなく、構造体を要素とする配列やスライスにまとめるのが自然です。ここではスライスを例に示しますが、配列でも同じ書き方で扱えます。書き方は以下のとおりです。要素の初期化子で型名は省略できます。

[]型名{
    {フィールド名: 値, ...},
    {フィールド名: 値, ...},
}

次のコードは、3人のユーザを持つスライスを作って、それぞれの情報を表示するコードです。

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func main() {
    users := []User{
        {Name: "Taro", Age: 30},
        {Name: "Jiro", Age: 25},
        {Name: "Hanako", Age: 28},
    }

    for _, u := range users {
        fmt.Printf("%s (%d歳)\n", u.Name, u.Age)
    }
}

以下のような実行結果が表示されます。

Taro (30歳)
Jiro (25歳)
Hanako (28歳)

スライスの各要素が順に取り出され、名前と年齢のペアで表示されました。

コードを解説します。

users := []User{
    {Name: "Taro", Age: 30},
    {Name: "Jiro", Age: 25},
    {Name: "Hanako", Age: 28},
}

[]User は「User のスライス」を表す型です。要素として3人分の User を並べて初期化しています。要素の初期化子で 型名 User を省略できます({Name: ..., Age: ...} だけで書ける)。

for _, u := range users {
    fmt.Printf("%s (%d歳)\n", u.Name, u.Age)
}

for-range でスライスの各要素を順に取り出し、u.Nameu.Age を書式付き出力しています。

冒頭で示したフィールドごとにスライスを持つ書き方と違い、1人分の情報がひとまとまりになっているので、フィールドを追加するときも User に追記するだけで済み、削除するときも該当要素を取り除くだけで整合性が崩れません。

4. メソッド

メソッド は、特定の型に紐づけられた関数です。関数名の前にレシーバと呼ばれる特別な引数を書くことで、「この関数は User 型のメソッド」といった具合に、その型のメソッドとして定義できます。データとそれに対する処理を、構造体とメソッドの組み合わせでひとまとまりで表現できるようになります。

📝 レシーバとは
メソッド定義の func と関数名の間に書く、(変数名 型) の形の特別な引数を レシーバ と呼びます。「この関数は〜型のメソッド」であることを宣言する役割を持ち、メソッドの中ではレシーバ変数を通してその型のフィールドにアクセスできます。

4.1 メソッドの定義

メソッドは通常の関数と似ていますが、func と関数名の間にレシーバを書く点が違います。書き方は以下のとおりです。

func (レシーバ変数 型) メソッド名(引数) 戻り値の型 {
    // 処理(レシーバ変数を通して型のフィールドにアクセスできる)
}

レシーバに書いた型のメソッドとして扱われ、インスタンス.メソッド名() の形で呼び出せます。

実際に試してみましょう。次のコードは、これまで扱ってきた User 型に、Age に応じて「未成年」「成人」を返す Category メソッドを紐づける例です。

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func (u User) Category() string {
    if u.Age < 18 {
        return "未成年"
    }
    return "成人"
}

func main() {
    u := User{Name: "Taro", Age: 30}
    fmt.Println(u.Category())
}

以下のような実行結果が表示されます。

成人

u.Category() の呼び出しで、u.Age30 なので 成人 が返されて出力されました。

コードを解説します。

func (u User) Category() string {
    if u.Age < 18 {
        return "未成年"
    }
    return "成人"
}

func の直後の (u User)レシーバです。「この関数は User 型のメソッドである」ことを宣言しており、関数内では u.Ageu.Name のようにレシーバのフィールドにアクセスできます。

u := User{Name: "Taro", Age: 30}
fmt.Println(u.Category())

User のインスタンス u を作り、u.Category() とドット記法でメソッドを呼び出しています。

なお、メソッドのレシーバには、値で受け取るものと ポインタ で受け取るものの2種類があります。フィールドを書き換えるメソッドではポインタレシーバを使いますが、詳しくは次章のGoポインタで扱います。

4.2 コンストラクタ関数

構造体のインスタンスを作るときに、事前準備や検証などの初期化ロジックをまとめておきたい場合は、New〜 という名前の関数を用意するのがGoの慣用パターンです。書き方は以下のとおりです。

func New型名(引数) 型名 {
    return 型名{フィールド名: 値, ...}
}

実際に試してみましょう。次のコードは、User のインスタンスを作って返す NewUser を定義する例です。

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func NewUser(name string, age int) User {
    return User{Name: name, Age: age}
}

func main() {
    u := NewUser("Taro", 30)
    fmt.Println(u)
}

以下のような実行結果が表示されます。

{Taro 30}

NewUser から返された User のインスタンスが u に入り、そのまま表示されました。

コードを解説します。

func NewUser(name string, age int) User {
    return User{Name: name, Age: age}
}

戻り値の型を User としています。New〜 という命名は「〜を作って返す」意図が名前から伝わるGoの慣用パターンです。実際には、戻り値を *UserUser のポインタ)にすることも多いですが、ポインタは次章のGoポインタで扱います。

5. 合成と埋め込み

既存の構造体の機能を、別の構造体で再利用したい場面があります。Goでは、他の構造体を自分の一部として組み込む 合成(composition) の書き方でこれを実現します。

Goで合成を実現する書き方は大きく2つあります。

  • ネスト: 他の構造体をフィールドとして書き入れる方法。「User は Address を持っている」といった所有関係を自然に表現できる
  • 埋め込み: フィールド名なしで書き入れる方法。埋め込まれた型のフィールドやメソッドが「昇格」して、外側の型が直接持っているように使える

順に見ていきます。

5.1 ネスト

構造体の中に別の構造体を持たせることができます。書き方は以下のとおりです。フィールドの型として別の構造体名を書きます。

type 内側の型 struct {
    // ...
}

type 外側の型 struct {
    フィールド名 内側の型
    // ...
}

次のコードは、住所を表す AddressUser の中に持たせる例です。

package main

import "fmt"

type Address struct {
    City string
    Zip  string
}

type User struct {
    Name    string
    Age     int
    Address Address
}

func main() {
    u := User{
        Name: "Taro",
        Age:  30,
        Address: Address{
            City: "Tokyo",
            Zip:  "100-0001",
        },
    }

    fmt.Println(u)
    fmt.Println(u.Address.City)
}

以下のような実行結果が表示されます。

{Taro 30 {Tokyo 100-0001}}
Tokyo

u 全体を表示すると内部の Address もまとめて出力され、u.Address.City でネストしたフィールドに直接アクセスできました。

コードを解説します。

type User struct {
    Name    string
    Age     int
    Address Address
}

UserAddress フィールドの型として、別の構造体 Address を指定しています。これで User は Address を1つ内部に持つ構造になります。

Address: Address{
    City: "Tokyo",
    Zip:  "100-0001",
},

初期化するときは、Address フィールドにさらに Address{} の初期化子を渡します。

fmt.Println(u.Address.City)

ネストしたフィールドは . を続けて u.Address.City のように辿って参照します。

5.2 埋め込み

ネストは「User は Address を持っている」を自然に表現できますが、u.Address.City のように毎回ネストしたフィールドを辿る必要がありました。埋め込み を使うと、フィールドやメソッドを外側の型で直接使えるようになり、より短く書けます。書き方は以下のとおりです。ネストと違い、フィールド名を書かずに埋め込みたい型名だけを書きます。

type 外側の型 struct {
    埋め込む型
    // 他のフィールド
}

次のコードは、Animal の機能(Name フィールドと Speak メソッド)を Dog に埋め込みで再利用する例です。

package main

import "fmt"

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " が鳴きます"
}

type Dog struct {
    Animal
    Breed string
}

func main() {
    d := Dog{
        Animal: Animal{Name: "Pochi"},
        Breed:  "Shiba",
    }

    fmt.Println(d.Name)
    fmt.Println(d.Speak())
    fmt.Println(d.Breed)
}

以下のような実行結果が表示されます。

Pochi
Pochi が鳴きます
Shiba

DogAnimal を埋め込んでいるため、d.Animal.Name と書かずに d.Name で直接アクセスでき、Speak()d.Speak() で呼び出せました。

コードを解説します。

type Dog struct {
    Animal
    Breed string
}

Dog の1行目に Animal(フィールド名なし)を書いています。これが埋め込みで、Animal のフィールドとメソッドが Dog の中から直接見えるようになります。

d := Dog{
    Animal: Animal{Name: "Pochi"},
    Breed:  "Shiba",
}

埋め込んだ Animal を初期化するには、フィールド名として Animal を指定し、その値として Animal{...} を渡します。

fmt.Println(d.Name)
fmt.Println(d.Speak())

d.Animal.Name と書かずに d.Name で直接アクセスでき、Speak()d.Speak() として呼び出せます。埋め込みによって、Animal のフィールドとメソッドがあたかも Dog 自身のものであるかのように振る舞います。

埋め込みはあくまでフィールドとメソッドの委譲にすぎません。外側の型が埋め込んだ型そのものになるわけではなく、「その型を持っている」という関係を短く書けるようにする仕組みだと理解しておくと迷いにくくなります。

6. タグ

構造体のフィールドにはタグと呼ばれる文字列を付けられます。JSONへの変換やDBマッピングで使います。書き方は以下のとおりです。フィールドの型のあとにバッククォート ` で囲んで書きます。

type 型名 struct {
    フィールド名 型 `タグ名:"値"`
}

次のコードは、User のフィールドに JSON 用のタグを付ける例です。

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

型定義のみの例のため、この抜粋には実行結果はありません。タグの効果(encoding/json を使った JSON への変換)は後の章で実際に動かして確認します。

コードを解説します。

Name string `json:"name"`
Age  int    `json:"age"`

フィールドの型のうしろに、バッククォート ` で囲んだ文字列を書けます。これがタグです。json:"name" は「このフィールドを JSON に変換するときは name というキー名を使う」という指示になります。

このタグにより、encoding/json パッケージが User.Name を JSON 上では name に変換してくれます。これはGo標準ライブラリencoding/json)やGORM入門(GORM)で活用します。

7. まとめ

この章では、Goの構造体を学びつつ、実際に構造体でデータと振る舞いをまとめる書き方を体験しました。

  • type 名前 struct { ... } で複数のフィールドを1つの型にまとめられる
  • 構造体をフィールドに持たせるとネストでき、u.Address.City のようにドットを辿って参照できる
  • 構造体のスライス([]User など)で、複数のインスタンスをまとめて扱える
  • フィールドに付けたタグは、encoding/json などのパッケージがメタ情報として読み取り、JSONキー名やDBカラム名の対応に使われる
  • func (u User) Category() ... の形で構造体にメソッドを紐づけられる
  • 初期化ロジックをまとめたいときは、New〜 という命名の関数(コンストラクタ関数)を用意するのがGoの慣用パターンである
  • 埋め込み(合成の一形態)を使うと、既存の構造体のフィールドとメソッドを外側の型から直接扱える

次の章では、Goのポインタを学びつつ、実際に &* の使い方、関数やメソッドのポインタ受け取りを体験します。

この教材は役に立ちましたか?

いいねをたくさんいただけると、制作者の励みになり、より多くのセクションが作れるようになります。

感想を一言(任意)

いただいたコメントは次の制作のヒントになります。ぜひお気軽にご投稿ください。

このコメントは他の受講生には公開されません。DevOps Camp運営が、教材改善のために確認します。

0 / 2000