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.log1. 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→ 0string→ ""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 のクラスメソッドとの対応
JS Go class Rect { area() {...} }func (r Rect) Area() float64 {...}this.widthr.Widthnew 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 特有の概念。
レシーバの使い分けルール(実務)
迷ったらポインタレシーバ にする。理由:
- 大きな struct のコピーを避ける(数 KB の struct もある)
- メソッドの一貫性: 同じ型のメソッドは値・ポインタを混ぜない方がいい
- 状態を変えるメソッドは必ずポインタレシーバ(値レシーバだと変更が消える)
値レシーバを選ぶ場面:
- 小さくて変更不要な値(
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 との対比
JS Go new User("Alice")user.New("Alice")class User { constructor() {} }func NewUser() *UserGo 標準ライブラリでも
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 class Go 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 class Go 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通常のメソッド static static 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. セッション録画を終了
exit2. 今日の発見(このノートに追記)
- 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) // OKembedding は「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
観点 interface struct embedding 中身 メソッド宣言のみ フィールド+メソッド 多態 異なる型を1つの interface で扱える 1つの型に固定 用途 振る舞いの契約 共通フィールドの再利用 例 io.Reader、errorTimestamps埋め込み詳細は 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:”-““ |
| Marshal | json.Marshal(u) |
| Unmarshal | json.Unmarshal(data, &u) |
「実務OK」基準
- API リクエスト/レスポンス用の struct を書ける: タグ込みで
- 値レシーバとポインタレシーバを迷わず選べる: 「迷ったらポインタ」
- NewXxx 関数を作って初期化ロジックを集約できる
- embedding で共通フィールド・メソッドを再利用できる
json:"-"でパスワード等を漏らさない: セキュリティ意識
次のレッスン
ここまでの全てを使って、動く CLI アプリ を作る。os.Args、flag パッケージ、ファイル読み書き、エラー処理を組み合わせて簡単な TODO CLI を完成させる。
つながりの予告
- 本章の
Todo struct + json タグが次章の ファイル永続化フォーマット になる NewTodo()のコンストラクタ慣習を CLI アプリで実践- ポインタレシーバが 状態を持つ TODO リスト操作 で活躍