Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Miyazaki.go 勉強会 #4

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

Miyazaki.go 勉強会 #4

「Learning Go With Tests(日本語訳: テスト駆動開発でGO言語を学びましょう)」の輪読会スライド

Avatar for Radish

Radish

May 15, 2026

More Decks by Radish

Other Decks in Programming

Transcript

  1. テストで学ぶ Go の構造体・メソッド インターフェース Learn Go with Tests — 「Structs,

    methods & interfaces 」解説 TDD で図形の面積を実装しながら struct → method → interface を学ぶ
  2. このスライドのゴール 🟦 Rectangle (長方形)から始める 🟢 Circle (円)を追加する 🔺 Triangle (三角形)まで拡張する

    その過程で、Go の struct / method / interface が なぜ必要になるのか を体験する。 TDD (テスト駆動開発)で「図形の面積・周囲長を計算するプログラム」を育てる。 出典: Learn Go with Tests — Structs, methods & interfaces
  3. このスライドの読み方 🔴 Red — まず失敗するテストを書く 🟢 Green — テストを通す最小の実装をする 🔵

    Refactor — テストを緑に保ったまま設計を改善する TDD は短いサイクルを回す。本資料は次のバッジで段階を示す。 「困りごと」スライドは、次の言語機能が必要になる転換点を表す。
  4. ゴールの姿(先に結論) 共通の振る舞いを抽象化 1 つのテストで全図形を検証 type Shape interface { Area() float64

    } areaTests := []struct { name string shape Shape hasArea float64 }{ {"Rectangle", Rectangle{12, 6}, 72.0}, {"Circle", Circle{10}, 314.1592653589793}, {"Triangle", Triangle{12, 6}, 36.0}, } ここに至るまでを TDD で一歩ずつたどる。
  5. 🔴 Perimeter — まず失敗するテスト 仕様 長方形の周囲の長さ = 2 × (幅

    + 高さ) 幅 10 、高さ 10 なら 40.0 。 テストを先に書く func TestPerimeter(t *testing.T) { got := Perimeter(10.0, 10.0) want := 40.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } Perimeter はまだ存在しない → コンパイルエラーで Red
  6. 🟢 Perimeter — 通す最小の実装 func Perimeter(width float64, height float64) float64

    { return 2 * (width + height) } テスト実行 → PASS 。最小限だけ書く(YAGNI ) 。
  7. 🔴🟢 Area — 同じリズムでもう一つ 🔴 テスト 🟢 実装 func TestArea(t

    *testing.T) { got := Area(12.0, 6.0) want := 72.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } func Area(width float64, height float64) float64 { return width * height } 面積 = 幅 × 高さ。12 × 6 = 72.0 で PASS 。
  8. 🔵 立ち止まる — float64 引数の問題 引数が ただの数値で、意図がコードに表れない 幅と高さを 逆に渡してもコンパイラは気づかない 「長方形」という概念がコードのどこにもない

    👉 データに名前と型を与えたい → struct へ Perimeter(10.0, 10.0) // この 2 つの 10.0 は何? Area(12.0, 6.0) // 幅と高さ、どっちが先?
  9. Rectangle 型を定義する 定義 生成とフィールドアクセス type Rectangle struct { Width float64

    Height float64 } r := Rectangle{Width: 10.0, Height: 10.0} r := Rectangle{10.0, 10.0} // 順序指定も可 r.Width // 10.0 r.Height // 10.0 ドット . でフィールドを読む。
  10. 🔵 Refactor — Rectangle を受け取る Before After func TestArea(t *testing.T)

    { got := Area(12.0, 6.0) // ... } func Area(width, height float64) float64 { return width * height } func TestArea(t *testing.T) { rectangle := Rectangle{12.0, 6.0} got := Area(rectangle) // ... } func Area(rectangle Rectangle) float64 { return rectangle.Width * rectangle.Height } Perimeter も同様に Rectangle を受け取る形へ。
  11. 困りごと — 円の面積も欲しい やりたいこと 長方形と同じく、円にも Area が欲しい。 しかし… Go は

    関数のオーバーロードができない (同名関数 を引数違いで複数定義できない) 。 func Area(rectangle Rectangle) float64 {/* ... */} func Area(circle Circle) float64 {/* ... */} ./shapes.go: Area redeclared in this block どう解決する?
  12. 解決の選択肢 案 A: 別パッケージに分ける rect.Area() と circle.Area() を別パッケージ に。 図形が増えるたびパッケージが増える

    「同じ Area 」という共通性が表せない → 大げさ 案 B: メソッドにする ✅ 型ごとに Area という メソッドを持たせる。 rectangle.Area() / circle.Area() 名前は同じでも 型に紐づくので衝突しない Go らしい解 → メソッドを採用
  13. メソッドとレシーバ 構文 (r Rectangle) が レシーバ 「この型に属する関数」を表す r はレシーバ変数(慣習で型の頭文字) 関数との違い

    func (r Rectangle) Area() float64 { return r.Width * r.Height } // 関数 Area(rectangle) // メソッド rectangle.Area() クラスではなく 型そのものにメソッドを定義する。
  14. 🔴 Circle — 失敗するテスト t.Run("circles", func(t *testing.T) { circle :=

    Circle{10} got := circle.Area() want := 314.1592653589793 if got != want { t.Errorf("got %g want %g", got, want) } }) Circle も Area() もまだない → Red 。 書式が %g なのは浮動小数点を簡潔に表示するため。
  15. 🟢 Circle — 実装 型とメソッド Rectangle もメソッド化 import "math" type

    Circle struct { Radius float64 } func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius } type Rectangle struct { Width float64 Height float64 } func (r Rectangle) Area() float64 { return r.Width * r.Height } 両者とも Area() を持つようになった。
  16. 型に機能が宿る データ(struct )と振る舞い(method )が 同じ型に集まった 呼び出し側は「その図形に面積を尋ねる」と読める Area(rectangle) // Before: 関数に渡す

    rectangle.Area() // After: 型が機能を持つ circle.Area() // 同じ書き方で円も 次の問い: 「どんな図形でも面積を検証する」テストは書けるか?
  17. 困りごと — 検証ロジックの重複 同じ検証を 2 度書いている 共通ヘルパーにしたいが… 引数の型は Rectangle ?

    Circle ? 両方を受け取り たい。 // rectangle 用 got := rectangle.Area() if got != want { t.Errorf(/* ... */) } // circle 用 got := circle.Area() if got != want { t.Errorf(/* ... */) } func checkArea(t testing.TB, shape ???, want float64) {/* ... */} 具体型では書けない → interface へ
  18. Shape インターフェース 定義 「 Area() float64 を持つものは すべて Shape で

    ある」 意味するもの interface は 「何ができるか」の約束 フィールドは持たない 実装の中身には関知しない type Shape interface { Area() float64 } required behaviour (必要な振る舞い)だけを宣言する。
  19. 🔵 Refactor — 共通ヘルパー checkArea サブテスト func TestArea(t *testing.T) {

    checkArea := func(t testing.TB, shape Shape, want float64) { t.Helper() got := shape.Area() if got != want { t.Errorf("got %g want %g", got, want) } } } t.Run("rectangles", func(t *testing.T) { rectangle := Rectangle{12, 6} checkArea(t, rectangle, 72.0) }) t.Run("circles", func(t *testing.T) { circle := Circle{10} checkArea(t, circle, 314.1592653589793) }) t.Helper() で失敗行を呼び出し側に表示。
  20. † 暗黙のインターフェース † Go Area() float64 を持つだけで 自動的に Shape を

    満たす。 Java / C# など implements Shape と 明示的に宣言する必要があ る。 type Rectangle struct { /* ... */ } func (r Rectangle) Area() float64 {/* ... */} // implements 宣言は不要! class Rectangle implements Shape { public double Area() {/* ... */} } シグネチャが一致すれば満たす(structural typing ) 。型と利用側 が疎結合になる。
  21. 疎結合 — 必要なことだけ要求する checkArea は 「 Area() を持つ何か」しか知らない Rectangle や

    Circle の具体的な中身に依存しない 新しい図形を足しても checkArea は一切変えなくてよい 「呼び出し側が必要とする最小の振る舞い」を interface で定義するのが Go の流儀。
  22. 困りごと — サブテストの繰り返し 図形が増えるたびに 似た t.Run ブロックをコピペ 検証の本質(shape と期待値)だけが違う 👉

    ケースを データの表にして 1 ループで回す t.Run("rectangles", func(t *testing.T) { /* ... */ }) t.Run("circles", func(t *testing.T) { /* ... */ }) t.Run("triangles", func(t *testing.T) { /* ... */ }) // 増え続ける
  23. テーブル駆動テスト ケースの表 1 つのループで回す areaTests := []struct { name string

    shape Shape hasArea float64 }{ { name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0, }, { name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793, }, } for _, tt := range areaTests { t.Run(tt.name, func(t *testing.T) { got := tt.shape.Area() if got != tt.hasArea { t.Errorf("%#v got %g want %g", tt.shape, got, tt.hasArea) } }) } 無名 struct のスライス = ケース一覧。
  24. テーブル駆動テストの利点 ケース追加が宣言 1 行 — 表に 1 要素足すだけ 見通しが良い —

    入力と期待値が一覧で並ぶ 失敗を特定しやすい — t.Run(tt.name, ...) でケース名が出る テストの拡張・保守コストが大きく下がる。
  25. 🔴 表に Triangle を 1 行足す areaTests := []struct {

    name string shape Shape hasArea float64 }{ {name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0}, {name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793}, {name: "Triangle", shape: Triangle{Base: 12, Height: 6}, hasArea: 36.0}, } Triangle 型がまだない → Red 。
  26. 🟢 Triangle — 追加するのはこれだけ 型とメソッド 変更しなかったもの Shape interface — 無改修

    checkArea / テストループ — 無改修 Rectangle / Circle — 無改修 type Triangle struct { Base float64 Height float64 } func (t Triangle) Area() float64 { return (t.Base * t.Height) * 0.5 } 型を 1 つ足すだけで全テストが通る = 設計の勝利。
  27. エラーメッセージを改善する %g と %#v %g — 浮動小数点を簡潔に %#v — struct

    の中身を Go 構文で表示 失敗時の出力例 どのケースの、どんな値で落ちたか 一目で分かる。 t.Errorf("%#v got %g want %g", tt.shape, got, tt.hasArea) main.Rectangle{Width:12, Height:6} got 72 want 73 良いテストは、良いエラーメッセージまで含む。
  28. 学びの総括 言語機能 struct — 関連データを束ね、意図を型で表す method — 型に振る舞いを持たせる interface (暗黙)

    — 必要な振る舞いだけを要求 し疎結合に テスト設計 table-driven tests — ケースをデータ化、追加は 1 行 エラーメッセージ — %#v / %g でデバッグ容易に TDD — Red → Green → Refactor で設計が育つ 機能は「困りごと」を解くために順に現れた。