1-6. 構造体とメソッド - struct, レシーバ, embedding, タグ

所要時間: 30-50分(がっつりなら2セッション分) コミット内容: ~/learn/go/day06/structs/ 一式


このレッスンのゴール

  • type User struct { ... } でデータ型を定義できる
  • 値レシーバ と ポインタレシーバ を使い分けられる
  • NewUser(...) *User のコンストラクタ慣習で初期化を集約できる
  • embedding で共通フィールド・メソッドを再利用できる
  • json:"name" json:"-" json:"...,omitempty" のタグが書ける

なぜ学ぶか

TS の class が無くてどう書くの?」という疑問への答えが本章。Go では データ(struct)と振る舞い(メソッド)を分離 して、必要に応じて組み合わせる。これにより「API レスポンス構造体に DB の関心が混ざる」「ORM の magic field がコードを読みにくくする」といった OOP の典型的な負債を回避できる。さらに json:"-" でパスワードを漏らさない セキュリティの基本パターン も本章で身につく。

前章とのつながり

1-5_スライスとマップmap[string]int を扱ったが、本章では map[string]User のような「自分で定義した型のコレクション」が書けるようになる。[]User のような 構造体のスライス は DB クエリ結果として最頻出。

これができると何が嬉しいか

  • REST API のリクエスト・レスポンス型 が美しく書ける(次レベルで必須)
  • DB のレコード ⇔ 構造体 のマッピングが書ける
  • パスワード等の機密フィールドを JSON 出力から排除 できる - セキュリティの基本
  • embedding で共通ロジック(タイムスタンプ、ロギング) を全エンティティに横展開

ストーリー導入: クラスは無い、データと関数で考える

JS の class User { hello() {} } を「箱に詰めて運ぶ」イメージとすると、Go の struct + メソッドは「データの袋(struct)と、それを処理するスタッフ(関数)」が別々に居る図。スタッフは「俺はこの袋を扱える」とレシーバ宣言する: func (u *User) Hello()。これだけ。継承(extends)は無く、必要なら別の袋を中に入れて「お裾分け」(embedding)する

OOP からの「データと振る舞いの分離」転換

JS だと「クラス設計が上手な人」が良いコードを書ける。 Go だと「データ構造をフラットに保ち、振る舞いは関数に切り出せる人」が良いコードを書ける。 同じ脳ではない。OOP の癖(is-a、深い継承)を一度脇に置く。


大前提: Go にクラスは無い、ある(の)は struct とメソッド

JS/TS は class を持つ。Go は意図的に持たない。代わりに:

  • struct: フィールドの集まり(データ)
  • メソッド: struct に紐づく関数(振る舞い)
  • embedding: 「継承」の代替(コンポジション)
  • interface: 振る舞いの契約(次レベルで詳述)

「データ」と「振る舞い」を分けて、明示的に組み合わせる。「is-a」より「has-a」を優先する文化

これが Go の「シンプル」の本質。クラスが無いと不便そうに見えるが、struct + メソッド + interface だけで現代的なバックエンドを綺麗に書ける。


セッション①: struct とメソッド(25-30分)

0. 録画スタート&作業ディレクトリ

mkdir -p ~/log ~/learn/go/day06/structs
cd ~/learn/go/day06/structs
go mod init example.com/structs
script ~/log/go_day06.log

1. struct の宣言

package main
 
import "fmt"
 
// 構造体を定義
type User struct {
	ID    int
	Name  string
	Email string
	Age   int
}
 
func main() {
	// 1. フィールド名を指定して作成(推奨)
	u1 := User{
		ID:    1,
		Name:  "Alice",
		Email: "alice@example.com",
		Age:   30,
	}
	fmt.Println(u1)
 
	// 2. 順番依存(フィールド追加で壊れるので非推奨)
	u2 := User{2, "Bob", "bob@example.com", 25}
	fmt.Println(u2)
 
	// 3. ゼロ値で作る
	var u3 User
	fmt.Println(u3) // {0  0} ← 全フィールドがゼロ値
 
	// 4. アクセス
	fmt.Println(u1.Name)
	u1.Age = 31
}

type 名前 struct { ... } の本質

型に名前を付けて、フィールドをまとめる」構文。type キーワードは struct 以外にも使える(後で interface 等で再登場)。

フィールドの可視性

  • 大文字始まり: 公開(他パッケージから見える)
  • 小文字始まり: 非公開(同じパッケージのみ)

関数と同じルール。JSON エンコードや DB ライブラリは公開フィールド(大文字始まり)しか見えない ので注意。

struct の初期化方法

書き方評価
User{ID: 1, Name: "A"}推奨。フィールド追加に強い
User{1, "A", "@", 30}順番依存。フィールド追加で壊れる
var u Userゼロ値で初期化。後で代入していく
u := &User{Name: "A"}ポインタで作る(実務で多い)
u := new(User)ポインタで作る(古めの書き方)

実務での型: ほぼ &User{Field: value}*User(ポインタ)として持ち回る。理由は次のメソッド節で説明。

struct のゼロ値

何も初期化せず var u User すると、全フィールドがそれぞれの型のゼロ値で埋まる:

  • int → 0
  • string → ""
  • bool → false
  • ポインタ・スライス・マップ → nil

「ゼロ値で意味のあるAPI」が Go 流: sync.Mutex{} bytes.Buffer{} などは new せずに var b bytes.Buffer だけで使える。

2. メソッド(レシーバ付き関数)

type Rectangle struct {
	Width  float64
	Height float64
}
 
// 値レシーバ - r は Rectangle のコピー
func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}
 
// ポインタレシーバ - r は元を指すポインタ
func (r *Rectangle) Scale(factor float64) {
	r.Width *= factor
	r.Height *= factor
}
 
func main() {
	rect := Rectangle{Width: 3, Height: 4}
	fmt.Println(rect.Area()) // 12
 
	rect.Scale(2) // ポインタレシーバでも普通に呼べる(Go が自動で & を付ける)
	fmt.Println(rect) // {6 8}
}

メソッドの本質

特定の型に紐づいた関数」。普通の関数との違いは、関数名の前に (レシーバ) を書くこと。

func (受け取る変数 型) メソッド名(引数) 戻り値 {
	// レシーバを self/this のように使える
}

JS のクラスメソッドとの対応

JSGo
class Rect { area() {...} }func (r Rect) Area() float64 {...}
this.widthr.Width
new Rect()Rect{} または &Rect{}

大きな違い: メソッドは struct の宣言から離れた場所に書ける。1つのファイルにフィールド、別ファイルにメソッドを置けるし、別の.go ファイルに分散もできる(同じパッケージ内であれば)。

3. 値レシーバ vs ポインタレシーバ

// 値レシーバ - コピーを受け取る
func (u User) Hello() string {
	return "Hello, " + u.Name
}
 
// ポインタレシーバ - 元を指すアドレスを受け取る
func (u *User) SetName(name string) {
	u.Name = name
}
 
func main() {
	u := User{Name: "Alice"}
	fmt.Println(u.Hello()) // Hello, Alice
 
	u.SetName("Bob")
	fmt.Println(u.Name)    // Bob ← 変更が反映されている
}

値レシーバとポインタレシーバの違い

観点値レシーバ (u User)ポインタレシーバ (u *User)
コピーコピーが作られるコピーされない(アドレス渡し)
フィールド変更元に反映されない元に反映される
速度構造体が大きいと遅い常に8バイトのアドレス渡しで速い
nil 安全性nil を渡せない(パニック前にコンパイル時に防げる)nil 受け取り得る(チェックが必要)

JS/TS で言うと: JS のクラスメソッドは常に this で元のオブジェクトを参照(ポインタレシーバ相当)。値レシーバは Go 特有の概念。

レシーバの使い分けルール(実務)

迷ったらポインタレシーバ にする。理由:

  1. 大きな struct のコピーを避ける(数 KB の struct もある)
  2. メソッドの一貫性: 同じ型のメソッドは値・ポインタを混ぜない方がいい
  3. 状態を変えるメソッドは必ずポインタレシーバ(値レシーバだと変更が消える)

値レシーバを選ぶ場面:

  • 小さくて変更不要な値(time.Time など標準ライブラリの “値型”)
  • スレッドセーフを暗黙に保証したい(コピーされるので並行アクセスでも安全)

チーム規約として「ポインタレシーバで統一」を推奨するプロジェクトも多い。

レシーバの落とし穴

  • 値レシーバのメソッド内で u.X = 1 しても反映されない: u はコピーだから。これに気付かず数時間溶かす罠
  • メソッドセットの違い: ポインタレシーバのメソッドは *User のメソッドセットに入り、User 単体には完全には属さない。interface 実装時に問題化することがある(中級者になってから気にすればOK)
  • 同じ型に値・ポインタレシーバ混在は避ける: コードベース全体の一貫性が崩れる

4. コンストラクタ関数

// Go には new キーワードがあるがあまり使わない
// 代わりに NewXxx という関数を作るのが慣習
 
func NewUser(name, email string) *User {
	return &User{
		Name:  name,
		Email: email,
		Age:   0, // デフォルト
	}
}
 
func main() {
	u := NewUser("Alice", "alice@example.com")
	fmt.Println(u)
}

NewXxx 関数の慣習

Go には「コンストラクタ」言語機能は無い。代わりに New + 型名 で関数を1つ書くのが規約。

規約

  • 名前は New + 型名(同パッケージ内なら NewUser、外部なら user.New が好まれる)
  • 戻り値は ポインタ*User
  • 必要なら初期値検証・デフォルト設定もここで行う

JS との対比

JSGo
new User("Alice")user.New("Alice")
class User { constructor() {} }func NewUser() *User

Go 標準ライブラリでも bufio.NewReader, http.NewRequest などこの規約。


セッション②: embedding と タグ(25-30分)

5. struct の埋め込み(embedding)

type Animal struct {
	Name string
	Age  int
}
 
func (a Animal) Describe() string {
	return fmt.Sprintf("%s (%d歳)", a.Name, a.Age)
}
 
// Animal を埋め込む
type Dog struct {
	Animal   // フィールド名なし = 埋め込み
	Breed string
}
 
func main() {
	d := Dog{
		Animal: Animal{Name: "ポチ", Age: 3},
		Breed:  "柴犬",
	}
 
	// 埋め込み元のフィールドが直接見える
	fmt.Println(d.Name)        // ポチ
	fmt.Println(d.Age)         // 3
	fmt.Println(d.Describe())  // ポチ (3歳) ← メソッドも継承的に使える
 
	// もちろん明示的にも書ける
	fmt.Println(d.Animal.Name) // ポチ
}

embedding の本質

他の型をフィールドとして名前なしで入れる」と、その型のフィールド・メソッドが自分のもののように使える。これが Go の継承の代替。

ただし継承ではない。コンポジション(has-a)の構文糖。「Dog Animal である」ではなく「Dog の中に Animal が居る」。

JS の継承との対比

観点JS classGo embedding
構文class Dog extends Animalstruct Dog { Animal; ... }
super 呼び出しsuper.method()d.Animal.Method()(明示的)
親の型として渡せるはい(is-a)いいえ(has-a)
多重単一継承のみ複数 embedding 可

embedding の実務ユースケース

  • 共通フィールドの再利用: TimestampedModel { CreatedAt, UpdatedAt } を全エンティティに埋め込む
  • HTTP ハンドラの拡張: 既存ハンドラを wrap してロギング追加
  • mutex 内蔵: struct { sync.Mutex; ... }obj.Lock() が直接呼べる
  • インターフェース実装の使い回し: 別の型のメソッドを「持ち込んで」一部だけ override

embedding の落とし穴

  • 継承ではない: func f(a Animal)d Dog を直接渡せない。f(d.Animal) のように明示
  • 同名フィールド・メソッドの衝突: 2つの型を埋め込んで両方に Name があると、外側からは曖昧でアクセスできなくなる
  • メソッドのオーバーライドは「再定義」: Dog に func (d Dog) Describe() を書くと、Dog 経由では Animal の Describe は隠れる(呼べなくはない、d.Animal.Describe()

6. 構造体タグ(JSON など)

package main
 
import (
	"encoding/json"
	"fmt"
)
 
type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email,omitempty"`
	Age   int    `json:"-"` // 出力に含めない
}
 
func main() {
	u := User{ID: 1, Name: "Alice", Email: "alice@example.com", Age: 30}
 
	data, _ := json.Marshal(u)
	fmt.Println(string(data))
	// {"id":1,"name":"Alice","email":"alice@example.com"}
 
	// JSON → struct
	jsonStr := `{"id":2,"name":"Bob"}`
	var u2 User
	json.Unmarshal([]byte(jsonStr), &u2)
	fmt.Printf("%+v\n", u2) // {ID:2 Name:Bob Email: Age:0}
}

構造体タグの本質

フィールドの隣に バッククォート で囲んだ文字列を書ける。リフレクション(reflect パッケージ)経由で読み取れる「メタデータ」。

Name string `json:"name" db:"user_name" validate:"required"`

タグは複数のキー(json, db, validate など)を スペース区切り で並べる。各ライブラリが自分のキーを読み取る。

主要なタグ

タグ用途ライブラリ
json:"name"JSONのキー名encoding/json(標準)
json:"name,omitempty"ゼロ値なら省略同上
json:"-"JSON 出力に含めない同上
db:"user_name"DBカラム名sqlx, gorm 等
validate:"required,email"入力検証go-playground/validator
yaml:"name"YAMLキー名go-yaml

使い所: API リクエスト/レスポンスの JSON マッピング、DB の ORM、設定ファイル読み込み、入力バリデーション。バックエンドの主要シナリオで全部必要

タグの落とし穴

  • 小文字始まりのフィールドはタグがあっても JSON 出力に出ない: 必ず大文字始まり(公開)に
  • タグの構文ミスでもコンパイルエラーにならない: 静的解析(go vet)で検出される
  • バッククォートを使う: "json:..." のダブルクォートだとエスケープが必要で見づらい

実務でよくある形

type CreateUserRequest struct {
	Name  string `json:"name" validate:"required,min=1,max=100"`
	Email string `json:"email" validate:"required,email"`
	Age   int    `json:"age,omitempty" validate:"gte=0,lte=150"`
}
 
type User struct {
	ID        int       `json:"id" db:"id"`
	Name      string    `json:"name" db:"name"`
	Email     string    `json:"email" db:"email"`
	Password  string    `json:"-" db:"password_hash"` // JSON 出力には絶対出さない
	CreatedAt time.Time `json:"created_at" db:"created_at"`
}

パスワードは json:"-" でレスポンスから絶対漏らさない。セキュリティの基本パターン

7. JS のクラスとの違いまとめ

JS class と Go struct の対比

観点JS classGo struct
文法class User { }type User struct { }
フィールドthis.name = "..."u.Name = "..."
コンストラクタconstructor()NewUser() 関数(規約)
メソッドclass { hello() { } }func (u *User) Hello()
継承extends Animalembedding(is-aじゃない)
super 呼び出しsuper.method()u.Animal.Method()(明示)
private#field または _小文字始まり(パッケージ単位)
ゲッター/セッターget/set通常のメソッド
staticstatic method()パッケージ関数で代用

最大の認識転換: Go では「クラスが世界の中心」じゃない。関数とパッケージが中心 で、struct はその中のデータ単位。**「OOP で考える前にデータと関数で考える」**のが Go 流。


練習課題

package main
 
import (
	"encoding/json"
	"fmt"
	"time"
)
 
// 共通の Timestamps を埋め込み
type Timestamps struct {
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}
 
func (t *Timestamps) Touch() {
	t.UpdatedAt = time.Now()
}
 
// ユーザー
type User struct {
	Timestamps        // embedding
	ID         int    `json:"id"`
	Name       string `json:"name"`
	Email      string `json:"email"`
	Password   string `json:"-"` // 出力に出さない
}
 
// 商品
type Product struct {
	Timestamps        // embedding
	ID         int     `json:"id"`
	Name       string  `json:"name"`
	Price      float64 `json:"price"`
}
 
func NewUser(name, email, password string) *User {
	now := time.Now()
	return &User{
		Timestamps: Timestamps{
			CreatedAt: now,
			UpdatedAt: now,
		},
		Name:     name,
		Email:    email,
		Password: password,
	}
}
 
func (u *User) Rename(newName string) {
	u.Name = newName
	u.Touch() // 埋め込みのメソッドが直接呼べる
}
 
func main() {
	u := NewUser("Alice", "alice@example.com", "secret123")
	fmt.Printf("作成時: %+v\n", u)
 
	time.Sleep(time.Second)
	u.Rename("Alicia")
	fmt.Printf("変更後: %+v\n", u)
 
	// JSON 化(Password は出ない)
	data, _ := json.MarshalIndent(u, "", "  ")
	fmt.Println(string(data))
 
	// Product も同じパターンで
	p := &Product{
		Timestamps: Timestamps{CreatedAt: time.Now(), UpdatedAt: time.Now()},
		ID:         1,
		Name:       "コーヒー",
		Price:      450,
	}
	fmt.Printf("%+v\n", p)
}
go run .

学べるポイント

  • Timestamps複数の型に埋め込みできる(多重再利用)
  • Touch() メソッドが各型で自分のものとして使える
  • Password が JSON 出力から消える(json:"-"
  • NewUser でコンストラクタっぽく初期化

締め: 振り返り(10分)

1. セッション録画を終了

exit

2. 今日の発見(このノートに追記)

- JS のクラスと比べてどう感じた?:
- 値レシーバとポインタレシーバの使い分け、納得した?:
- embedding を「継承じゃない」と説明するなら?:
- 明日やりたいこと:

アンチパターン集 - やらかし事例

struct 設計のやらかし

1. パスワードを JSON に出してしまう

type User struct {
    Name     string
    Password string  // ← json:"-" がない
}
json.Marshal(u)  // {"Name":"...","Password":"hashed_secret"}

本番事故の定番。レスポンス struct には json:"-" で機密フィールドを除外、または DB 用と API レスポンス用 struct を分ける。

2. 値レシーバで状態を変えようとして変わらない

func (u User) SetName(n string) { u.Name = n }  // u はコピー
u := User{Name: "old"}
u.SetName("new")
fmt.Println(u.Name)  // "old" のまま

状態を変えるメソッドは 必ずポインタレシーバ (u *User)

3. レシーバが値とポインタで混在

func (u User) Hello() string { ... }      // 値
func (u *User) SetName(n string) { ... }  // ポインタ

プロジェクト全体で どちらかに統一 が読みやすい。go vet で警告される場合もある。

4. embedding を継承だと誤解

type Dog struct { Animal; Breed string }
func feed(a Animal) { ... }
feed(dog)  // NG: Dog は Animal ではない
feed(dog.Animal)  // OK

embedding は「is-a」ではなく「has-a」。型変換は不可。

5. 順番依存初期化でフィールド追加で全壊

u := User{1, "Alice", "alice@example.com"}  // ← フィールド追加でズレる

必ず User{ID: 1, Name: "Alice"} のフィールド名指定で。

対比表で違いを明確化

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

観点(u User)ポインタ (u *User)
コピーされるされない
フィールド変更反映されない反映される
速度大きい struct で遅い常に8バイト渡し
nil 安全性nil 不可nil 受け取り得る
用途小さい不変な型迷ったらこっち

interface vs struct embedding

観点interfacestruct embedding
中身メソッド宣言のみフィールド+メソッド
多態異なる型を1つの interface で扱える1つの型に固定
用途振る舞いの契約共通フィールドの再利用
io.ReadererrorTimestamps 埋め込み

詳細は Level 2 で深掘り。


自己評価チェックリスト

手を動かせた

  • type User struct { ... } を書いた
  • フィールド名指定で初期化した
  • 値レシーバ・ポインタレシーバ両方を書いた
  • NewUser(...) *User を書いた
  • embedding を使った
  • json:"name" json:"-" のタグを書いた
  • json.Marshal / Unmarshal を実行した

説明できる

  • 値レシーバとポインタレシーバの使い分け
  • NewXxx 慣習がなぜ広く使われるか
  • embedding が「継承ではない」理由
  • 大文字始まりフィールドだけが JSON に出る理由

やらかし回避

  • パスワード等を json:"-" で除外する習慣
  • 値レシーバで状態変更しようとする罠を認識
  • フィールド順番依存の初期化を避ける

詰まった時のチートシート

やりたいこと書き方
struct 宣言type User struct { Name string }
初期化u := User{Name: "A"}
ポインタで初期化u := &User{Name: "A"}
ゼロ値で宣言var u User
フィールドアクセスu.Name
メソッド(値レシーバ)func (u User) Hello() string {}
メソッド(ポインタレシーバ)func (u *User) SetName(n string) {}
コンストラクタ慣習func NewUser() *User { return &User{} }
埋め込みtype Dog struct { Animal; Breed string }
JSON タグName string \json:“name”“
JSON 省略\json:“email,omitempty”“
JSON 除外\json:”-““
Marshaljson.Marshal(u)
Unmarshaljson.Unmarshal(data, &u)

「実務OK」基準

  • API リクエスト/レスポンス用の struct を書ける: タグ込みで
  • 値レシーバとポインタレシーバを迷わず選べる: 「迷ったらポインタ」
  • NewXxx 関数を作って初期化ロジックを集約できる
  • embedding で共通フィールド・メソッドを再利用できる
  • json:"-" でパスワード等を漏らさない: セキュリティ意識

次のレッスン

1-7. CLI アプリを作る へ。

ここまでの全てを使って、動く CLI アプリ を作る。os.Argsflag パッケージ、ファイル読み書き、エラー処理を組み合わせて簡単な TODO CLI を完成させる。

つながりの予告

  • 本章の Todo struct + json タグ が次章の ファイル永続化フォーマット になる
  • NewTodo() のコンストラクタ慣習を CLI アプリで実践
  • ポインタレシーバが 状態を持つ TODO リスト操作 で活躍