Go基礎 #6 構造体とメソッド

読了 7分

#5 コレクション でデータを束ねる道具を見ました。今回は — 自分の意味を持った型を作る場面。structとメソッド。

Struct — ユーザー定義型 #

struct 基本
type User struct {
	ID   string
	Name string
	Age  int
}

func main() {
	u := User{
		ID:   "u1",
		Name: "カーティス",
		Age:  30,
	}
	fmt.Println(u)         // {u1 カーティス 30}
	fmt.Println(u.Name)    // カーティス
}

type 名前 struct { フィールドたち } で定義。フィールドごとに型を書きます。

作り方 — いくつかの方法 #

struct インスタンス
// フィールド名を明示 (推奨)
u1 := User{ID: "u1", Name: "カーティス", Age: 30}

// 一部のフィールドのみ (残りは zero value)
u2 := User{Name: "アリス"}

// 位置ベース (推奨しない — フィールド順序が変わると壊れる)
u3 := User{"u3", "ボブ", 35}

// zero value
var u4 User    // {  0}

// new でポインタ
u5 := new(User)   // *User, すべてのフィールドが zero

フィールド名を明示する形が標準です。位置ベースはフィールドを追加/再配置すると壊れやすいです。

フィールドアクセス — ドット表記 #

フィールドアクセス
u := User{Name: "カーティス", Age: 30}
fmt.Println(u.Name)
u.Age = 31

ポインタを通しても同じドット表記を使います — Goが自動で逆参照します。

ポインタからアクセス
p := &u
fmt.Println(p.Name)   // OK — (*p).Name だが自動で逆参照
p.Age = 32             // OK

メソッド — 型に紐づいた関数 #

メソッド基本
type User struct {
	Name string
	Age  int
}

func (u User) Greet() {
	fmt.Printf("こんにちは, %s (%d)\n", u.Name, u.Age)
}

func main() {
	u := User{Name: "カーティス", Age: 30}
	u.Greet()    // こんにちは, カーティス (30)
}

func (u User) Greet()(u User) 部分がレシーバ(receiver)。この関数はUser型に紐づいたメソッドです。

レシーバ名は通常型の最初の文字の小文字を使います(u for User)。this/selfではなく、毎回明示的に名前を書くのがGoの慣習です。

値レシーバ vs ポインタレシーバ #

ここが最初に最も混乱する場面です。

値レシーバ — コピーを受け取る #

値レシーバ
func (u User) IncrementAge() {
	u.Age++             // コピーの Age が増加 — 元は変わらない
}

u := User{Age: 30}
u.IncrementAge()
fmt.Println(u.Age)   // 30 (変化なし)

値レシーバは呼び出し時にstructが丸ごとコピーされます。メソッド内で変更しても元はそのまま。

ポインタレシーバ — 元を指す #

ポインタレシーバ
func (u *User) IncrementAge() {
	u.Age++             // 元の Age が増加
}

u := User{Age: 30}
u.IncrementAge()
fmt.Println(u.Age)   // 31

*User レシーバはポインタを受け取り — メソッド内の変更が元に反映されます。

どちらを使うか? #

ガイドライン:

  1. 値を変更する必要があれば → ポインタレシーバ
  2. structが大きければ → ポインタレシーバ (コピーコスト回避)
  3. 上記2つでなければ → 値レシーバ

ただし実務で最もよく使うガイドは — 一つの型内で一貫性を保つことです。あるメソッドはポインタレシーバ、別のメソッドは値レシーバのように混ぜると不自然なので、通常その型のすべてのメソッドを一種類に統一します。

Goの自動逆参照/ポインタ変換 #

呼び出し側ではこの2つをほとんど気にしません。

自動変換
u := User{Age: 30}
p := &u

u.PointerMethod()     // 自動的に (&u).PointerMethod()
p.ValueMethod()       // 自動的に (*p).ValueMethod()

呼び出し側が値かポインタか、Goコンパイラがよしなに変換してくれます。ただし変更が必要なら呼び出し側がポインタを持っている必要があります(スライスの要素のような一時的な位置だとポインタを取れないこともあります)。

コンストラクタ — New... 慣習 #

Goにはクラスや明示的なconstructorがありません。代わりにコンストラクタ関数を慣習として作ります。

New 関数パターン
type User struct {
	ID   string
	Name string
	Age  int
}

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

func main() {
	u := NewUser("カーティス", 30)
	fmt.Println(u.Name)
}

NewUserNewClient のように New型名 が標準的な名前です。通常はポインタを返します。

埋め込み — 合成で再利用 #

Goにはクラス継承がありませんが — 埋め込み(embedding) で似た効果を出せます。

埋め込み
type Animal struct {
	Name string
}

func (a Animal) Speak() {
	fmt.Println(a.Name, "が音を出します")
}

type Dog struct {
	Animal           // 埋め込み — フィールド名なしに型だけ
	Breed string
}

func main() {
	d := Dog{
		Animal: Animal{Name: "ボリ"},
		Breed:  "柴",
	}
	d.Speak()        // Animal のメソッドを直接使える
	fmt.Println(d.Name)   // Dog から直接 Name にアクセス
}

Dog の中の Animal が埋め込み — フィールド名なしに型だけ書きました。こうすると:

  • Animal のメソッド(Speak)を Dog から直接呼べる
  • Animal のフィールド(Name)を d.Name で直接アクセス

継承ではなく昇格(promotion) です — 外から見ればまるでDogが直接そのメソッド/フィールドを持っているかのように動作します。

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

同じ名前のメソッドをDogに定義すると — Dogのものが昇格されたメソッドを隠します。

オーバーライド
func (d Dog) Speak() {
	fmt.Println(d.Name, "が吠えます (ワンワン)")
}

d := Dog{Animal: Animal{Name: "ボリ"}, Breed: "柴"}
d.Speak()    // ボリ が吠えます (ワンワン)

// Animal のメソッドを明示的に呼ぶには
d.Animal.Speak()

JSのsuper.methodと似て — d.Animal.Speak() で親(?)のものを直接呼び出せます。

埋め込み vs 継承 — 違い #

伝統的なOOPの継承とは次の点が違います。

  • DogはAnimalを持っているのであって、Animalであるわけではない
  • 多重埋め込み可能 (type X struct { A; B })
  • 「is-a」関係ではなく 「has-a」+ メソッド昇格

Goの設計哲学の一つが — 継承より合成です。深いクラス階層を作れないように意図的に塞がれています。

Struct比較 #

同じ型の2つのstructは、すべてのフィールドが同じなら == がtrue。

struct 比較
u1 := User{Name: "カーティス", Age: 30}
u2 := User{Name: "カーティス", Age: 30}
u3 := User{Name: "アリス", Age: 25}

u1 == u2   // true
u1 == u3   // false

ただし — スライス/マップ/関数をフィールドに持つstructは比較不可です(コンパイルエラー)。こういう場面には reflect.DeepEqual または自前のEqualメソッド。

無名struct (anonymous struct) #

名前なしでその場で作るstruct。

無名 struct
config := struct {
	Host string
	Port int
}{
	Host: "localhost",
	Port: 8080,
}

fmt.Println(config.Host, config.Port)

テストケースや一時的なデータの集まりに向きます。繰り返し使うデータなら type で名前を付けた方がよいです。

Tag — メタデータ #

structフィールドにタグ(メタデータ)を付けられます。JSONシリアライズやDBマッピングで核心となる道具です。

JSON タグ
type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Age  int    `json:"age,omitempty"`
}

encoding/json パッケージがこのタグを読んで — Goフィールド名(ID)とJSONキー(id)をマッピングします。omitempty はzero valueなら結果から除外せよの意味。

中級 #7 標準ライブラリツアー実戦 #3 JSON で詳しく扱います。

よく使うパターン #

1) Builderパターン — メソッドチェイン #

builder パターン
type Query struct {
	table  string
	limit  int
	where  []string
}

func NewQuery() *Query {
	return &Query{}
}

func (q *Query) Table(t string) *Query {
	q.table = t
	return q
}

func (q *Query) Where(w string) *Query {
	q.where = append(q.where, w)
	return q
}

func (q *Query) Limit(n int) *Query {
	q.limit = n
	return q
}

// 使用
q := NewQuery().
	Table("users").
	Where("active = true").
	Limit(10)

各メソッドが自分自身を返すのでチェインが可能。ライブラリAPIでよく登場する形です。

2) オプションパターン — 関数型オプション #

コンストラクタに多くのオプションが必要なとき。

functional options
type Server struct {
	host    string
	port    int
	timeout time.Duration
}

type Option func(*Server)

func WithHost(host string) Option {
	return func(s *Server) { s.host = host }
}

func WithPort(port int) Option {
	return func(s *Server) { s.port = port }
}

func WithTimeout(d time.Duration) Option {
	return func(s *Server) { s.timeout = d }
}

func NewServer(opts ...Option) *Server {
	s := &Server{
		host:    "localhost",
		port:    8080,
		timeout: 30 * time.Second,
	}
	for _, opt := range opts {
		opt(s)
	}
	return s
}

// 使用
srv := NewServer(
	WithPort(3000),
	WithTimeout(60 * time.Second),
)

オプション関数で設定をまとめて受けるパターン。Goライブラリで標準に近く使われます。

まとめ #

今回の記事で整理した内容:

  • type X struct { ... } でユーザー定義型
  • インスタンス — フィールド名の明示が推奨
  • メソッド — func (r 型) Method() の形
  • 値レシーバ vs ポインタレシーバ — 変更必要/大きいstruct → ポインタ
  • 一つの型のメソッドは一種類に統一するのが自然
  • New... 慣習でコンストラクタを真似る
  • 埋め込みで合成 + メソッド昇格 — 継承ではない
  • 同じ型のstructは == 可能 (フィールドが比較可能なとき)
  • タグでメタデータ — JSON/DBマッピングに使用
  • builderパターン、functional optionsパターン

次の記事(#7 パッケージとモジュール)ではシリーズの最後として — コードを複数のファイル/パッケージに分け、go modで外部依存を扱う方法を整理します。

X