imu-note
いむたろ
いむたろ
@imutaroh
新卒エンジニア / データ基盤 × AI

構造体

名前と型の付いたフィールドをひとまとめにした型。

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.Namepanic。ポインターの入れ物を作っただけで、中身の 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 が結果を返して、呼び出し側で代入し直す形になっているのはこのため
  • 書き換えるメソッドはポインタレシーバーにすると決めておけば、この表を覚えなくても事故は起きない
  • 長さと容量の話は Goスライスの len・cap・nil

埋め込み

フィールド名を書かずに型名だけ書くと、埋め込みになる。

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 を短く書いたもの
  • 2つ埋め込んで両方に同じ名前のフィールドがあると、短い書き方はコンパイルエラー(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 のようなライブラリが読んで使う
  • 外の API は 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枚。ルーを外から渡す
名前テンプレートメソッドストラテジー

部品を外から渡す形なら、テストで偽物の部品を入れやすい。

関連