名前と型の付いたフィールドをひとまとめにした型。
type Address struct {
City string
}
type User struct {
Name string
Age int
Address Address // 部品の構造体には名前を付けて分けておく
}
u := User{Name: "taro", Age: 20} // 書かなかった Address はゼロ値
| 書き方 | 起きること |
|---|---|
var p *User のあと p.Name | panic。ポインターの入れ物を作っただけで、中身の User はどこにもない(nil) |
User{"taro", 20, Address{}} | 通るが、フィールドを足したり並べ替えたりすると壊れる。Name: のように名前を書く |
既定値を入れたい・検査したいなら New で始まるファクトリー関数で作る(2章の NewBatchSize と同じ)。
値レシーバーはコピーを受け取る。値を書き換えないっていう宣言になる
| レシーバー | 受け取るもの | 中で書き換えると |
|---|---|---|
値 (t Team) | コピー | コピーが変わって捨てられる。元は変わらない |
ポインタ (t *Team) | 元の場所 | 元が変わる |
値レシーバーで書き換えても、コンパイルエラーにも警告にもならない。「呼んだのに変わらない」ときはまずここを疑う。Goメソッドレシーバー(ポインタ vs 値)。
関数の中の変更を外に出す方法は2つ。
構造体の状態を変えるメソッドならポインタレシーバー。小さい値の計算なら新しい値を返す。 引数のポインターに結果を書き込んで返す使い方は、Go ではあまりしない。
値渡しなのに参照渡しの構造体があったら、値渡しの暗黙のルールである「実体を書き換えない」というのが使えなくなるので、これはレビューが来たら止める
type Team struct {
Name string
Members []string
}
func (t Team) Rename(name string) { t.Name = name } // 元に届かない
func (t Team) ReplaceFirst(m string) { t.Members[0] = m } // 元に届く
t := Team{Name: "A", Members: []string{"taro", "hanako"}}
t.Rename("B") // t.Name は A のまま
t.ReplaceFirst("jiro") // t.Members は [jiro hanako]
コピーされるのは、スライスが「どこを指しているか」だけ。 指している先の配列は元とコピーで共有する。マップ・ポインターのフィールドも同じ。
スライスの変数に入っているのは、中身そのものではなく、3つの項目が書かれた小さな札。
t.Members(札)
┌─────────────────────────┐
│ 場所:配列はここにある ──┼──▶ [ "taro", "hanako" ] ← 本体の配列
│ 長さ(len):2 │
│ 容量(cap):2 │
└─────────────────────────┘
構造体をコピーすると、コピーされるのは札だけ。札が2枚になり、両方が同じ配列を指す。マップも同じで、変数が持っているのは中身の表の場所だけ。
Go の代入は、いつも「変数に入っているもの」をそのままコピーする。スライスとマップは、入っているのが札なので、札はコピーされても中身は共有される。
type Team struct {
Members []string
Scores map[string]int
}
func (t Team) ReplaceFirst(m string) { t.Members[0] = m } // 中身を書き換える
func (t Team) AddMember(m string) { t.Members = append(t.Members, m) } // 要素を足す
func (t Team) SetScore(m string) { t.Scores[m] = 100 } // マップに書く
t := Team{Members: []string{"taro", "hanako"}, Scores: map[string]int{}}
t.ReplaceFirst("jiro")
t.AddMember("saburo")
t.SetScore("taro")
// t.Members → [jiro hanako] jiro は届き、saburo は届かない
// t.Scores → map[taro:100] 届く
| 操作 | 元に届くか | 理由 |
|---|---|---|
t.Members[0] = ... | 届く | 札の「場所」をたどって、共有している配列に書く |
t.Members = append(...) | 届かない | append が返した新しい札を、コピー側に代入しただけ。元の札は長さ2のまま |
t.Scores[k] = ... | 届く | 共有している表に書く |
t.〇〇 = ... とフィールドに丸ごと代入するのは届かない。t.〇〇[...] = ... と中身に書き込むのは届くappend すると「足したはずなのに消えた」、[0] = だと「変えないはずなのに変わった」になるappend が結果を返して、呼び出し側で代入し直す形になっているのはこのためフィールド名を書かずに型名だけ書くと、埋め込みになる。
type Profile struct {
Name string
}
func (p Profile) Greet() string { return "hi, " + p.Name }
type Admin struct {
Profile // 型名だけ=埋め込み
Level int
}
a := Admin{Profile: Profile{Name: "taro"}, Level: 1} // 作るときは型名で書く
a.Name // taro。内側のフィールドに直接届く
a.Greet() // hi, taro。メソッドも
a.Profile.Name // 同じもの。中身は「型名と同じ名前のフィールド」が1つあるだけ
a.Name は a.Profile.Name を短く書いたものambiguous selector)。s.Profile.Name のように明示するフィールドの後ろにバッククォートで書く文字列。外の世界の名前(JSON のキー・DB の列名)と、Go のフィールド名を対応付ける。
type Order struct {
UserID string `json:"user_id"` // JSON の user_id をここに入れる
Amount int `json:"amount,omitempty"` // ゼロ値なら出力しない
Password string `json:"-"` // JSON には出さない
}
// {"user_id":"u-1","amount":0} を読み込むと {UserID:u-1 Amount:0 Password:}
// Password に値を入れて書き出しても {"user_id":"u-1"}
encoding/json のようなライブラリが読んで使うuser_id のような小文字のスネークケース、Go の公開フィールドは大文字始まり。その食い違いをタグで埋めるencoding/json からは見えない| # | 決めること | 判断の材料 |
|---|---|---|
| 1 | ポインターで扱うか、値で扱うか | フィールドにマップ・スライス・ポインター・sync.Mutex があればポインター |
| 2 | 値で扱うなら、書き換えられるようにするか | 「ほぼ値そのもの」なら書き換えない。変更は新しい値を返す |
| 3 | ゼロ値のままで動くようにするか | するなら New を呼び忘れても壊れない |
// 1. マップを持つ → ポインターで扱う
type Visited struct {
ids map[string]bool
}
func NewVisited() *Visited {
return &Visited{ids: map[string]bool{}}
}
// 2. 書き換えない値。変更は新しい値を返す
type Money struct {
Amount int
}
func (m Money) Add(n int) Money {
m.Amount += n
return m
}
m := Money{Amount: 100}
m2 := m.Add(50) // m は 100 のまま、m2 は 150
// 3. ゼロ値のまま使える。nil のスライスにも append できる
type Cart struct {
items []string
}
func (c *Cart) Add(item string) {
c.items = append(c.items, item)
}
var c Cart // New を通さなくても動く
c.Add("apple")
マップを持つ構造体を値でコピーすると、コピー同士で同じマップを共有する。sync.Mutex を持つ構造体を値で渡すと、go vet が passes lock by value と警告する。
struct{} はフィールドが1つもない構造体。大きさは0バイト。データを運べないので、「中身は要らない」ことを型で伝えるのに使う。
done := make(chan struct{}) // 呼び出しベル
go func() { // 厨房。main と同時に動く
fmt.Println("料理中...")
close(done) // ベルを鳴らす
}()
<-done // ベルが鳴るまで、ここで止まって待つ
fmt.Println("運ぶ")
chan bool でも動くが、読む人が「true と false で違いがあるのか」と考えてしまうclose は待っている全員にいっぺんに届くcontext の ctx.Done() も <-chan struct{} で、同じ形seen := map[string]struct{}{}
for _, id := range []string{"a", "b", "a"} {
_, ok := seen[id]
if ok {
fmt.Println("重複:", id) // 重複: a
continue
}
seen[id] = struct{}{}
}
len(seen) // 2
知りたいのは「そのキーがあるか」だけ。map[string]bool でもできるが、false が入っているときの意味を考えずに済む。
クラスはデータと処理をひとまとめにした設計図(レシピ)。そのレシピで作った1つ1つの実体がインスタンス(1皿のカレー)。
クラスの言語には継承とオーバーライドがある。
継承っていらなくない?丸ごと引き継ぐなら呼び出せばいいだけじゃん
引き継ぐだけなら、呼び出せば済む。継承でしかできないのは、親の流れの途中で子の手順を使わせること(オーバーライド)だけ。
埋め込みを継承のつもりで使うと、差し替えが効かない。
type Curry struct{}
func (c Curry) Cook() string { return "切る→煮る→" + c.Season() }
func (c Curry) Season() string { return "中辛ルー" }
type SweetCurry struct{ Curry }
func (s SweetCurry) Season() string { return "甘口ルー" } // 差し替えたつもり
SweetCurry{}.Cook() // 切る→煮る→中辛ルー
Cook の中の c は Curry で、自分が SweetCurry に埋め込まれていることを知らない。埋め込みは継承ではない。
type Seasoner interface {
Season() string
}
type Recipe struct {
Seasoner Seasoner // 差し替え口
}
func (r Recipe) Cook() string { return "切る→煮る→" + r.Seasoner.Season() }
type Sweet struct{}
func (Sweet) Season() string { return "甘口ルー" }
type Hot struct{}
func (Hot) Season() string { return "辛口ルー" }
Recipe{Seasoner: Sweet{}}.Cook() // 切る→煮る→甘口ルー
Recipe{Seasoner: Hot{}}.Cook() // 切る→煮る→辛口ルー
| クラスの言語 | Go | |
|---|---|---|
| 違いを作る方法 | 子のレシピを作って、味付けを書き換える | レシピは1枚。ルーを外から渡す |
| 名前 | テンプレートメソッド | ストラテジー |
部品を外から渡す形なら、テストで偽物の部品を入れやすい。
このサイトのノートから、知りたいことが書いてあるページを探して案内するよ。
AI の案内はまちがうことがあるよ。中身はリンク先のノートで確かめてね。