01.5 ジェネリクス — 型をパラメータ化して再利用する¶
「int のスタック」「string のスタック」「自分で作った型のスタック」……。 同じ構造のスタックを型ごとに全部書くのは非効率です。
ジェネリクス(generics) を使うと、型をパラメータとして受け取る「型にとらわれない」コードを 1 回だけ書けます。Go では Go 1.18 から使えるようになりました。
このレッスンのゴール:
- 型パラメータ(
[T any])を書いてジェネリックな型・関数を作れる - ジェネリクス無し(
any+ 型アサーション)の危険を説明できる - 型安全性の利点を実演で示せる
1. ジェネリクス無しの世界: any と型アサーション¶
Go のジェネリクス以前は、any(= interface{})で「何でも入る箱」を作り、取り出す時に
型アサーションで元の型に戻すのが一般的でした。
item := stack.Pop() // any
n := item.(int) // 型アサーション
問題は、入れる時に間違えた型が混ざっても、取り出す時に初めて気づく(またはパニックする)ことです。
import (
"fmt"
"strings"
"github.com/janpfeifer/gonb/gonbui"
)
// AnyStack は any を入れる「型を失った」スタック。
// 入れる時は何でも入るが、取り出す時に型アサーションが必要。
type AnyStack struct {
items []any
}
func (s *AnyStack) Push(item any) {
s.items = append(s.items, item)
}
func (s *AnyStack) Pop() (any, bool) {
if len(s.items) == 0 {
return nil, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
%%
s := &AnyStack{}
s.Push(42) // int
s.Push("hello") // string ← 型がバラバラでも入ってしまう
s.Push(7)
v1, _ := s.Pop() // any で出てくる
fmt.Printf("Pop() の結果: %v (型: %T)\n", v1, v1)
n := v1.(int) // 型アサーションが必要
fmt.Println("型アサーションで int にした:", n)
// もし取り違えていたらパニックする:
// s.Push("hello")
// v, _ := s.Pop()
// _ = v.(int) // panic: interface conversion
Pop() の結果: 7 (型: int) 型アサーションで int にした: 7
ここが問題: AnyStack は「何が入っているか」を教えてくれません。取り出す度に
「本当に int か?」を自分で確認する(またはパニックを覚悟する)必要があります。
2. ジェネリックな型: Stack[T]¶
Stack[T any] と書くと、T を型パラメータとして受け取るスタックを定義できます。
Stack[int] を作れば、中身は int だけ。Stack[string] を作れば、string だけです。
// Stack[T] は型 T 専用のスタック。T は後から決められる。
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
ここがポイント¶
[T any]が型パラメータ。anyは「どんな型でも良い」という制約Stack[int]は int 専用、Stack[string]は string 専用の別々の型になる- 取り出す時に型アサーションが不要。コンパイラが型を保証してくれる
3. 実際に動かしてみる¶
Stack[int] と Stack[string] を作って、型安全性を確かめます。
func demoStacks() {
ints := &Stack[int]{}
ints.Push(10)
ints.Push(20)
// 型アサーション不要で int が直接出てくる
v, ok := ints.Pop()
fmt.Printf("Stack[int] Pop → %v (型: %T, ok=%v)\n", v, v, ok)
strs := &Stack[string]{}
strs.Push("hello")
s, ok := strs.Pop()
fmt.Printf("Stack[string] Pop → %v (型: %T, ok=%v)\n", s, s, ok)
// ジェネリクスなら混入がコンパイルエラーになる(次のセルで確認)
}
%%
demoStacks()
Stack[int] Pop → 20 (型: int, ok=true) Stack[string] Pop → hello (型: string, ok=true)
型の混入がコンパイルエラーになることを、// @ignore の代わりにコメントのコードで確認します。
実際に実行するとコンパイルが通らないので、コメントのままにしてあります。
ints := &Stack[int]{}
ints.Push(10)
ints.Push("hello") // ← コンパイルエラー: cannot use "hello" as int
ジェネリクス無し(AnyStack)とジェネリクス(Stack[T])の比較を表で見えます。
func compareTable() string {
var b strings.Builder
b.WriteString(`<table border="1" cellpadding="4" style="border-collapse:collapse">
<tr><th></th><th>AnyStack(any)</th><th>Stack[T](generics)</th></tr>`)
b.WriteString(`<tr><td>入れる時</td><td>何でも入る(型が失われる)</td><td>T だけ。違う型はコンパイルエラー</td></tr>`)
b.WriteString(`<tr><td>取り出す時</td><td>型アサーションが必要・失敗でパニック</td><td>型アサーション不要・型が保証される</td></tr>`)
b.WriteString(`<tr><td>コード量</td><td>1 つで済む</td><td>1 つで済む(int/string/自作型すべてに適用)</td></tr>`)
b.WriteString(`</table>`)
return b.String()
}
%%
gonbui.DisplayHTML(compareTable())
gonbui.Sync()
| AnyStack(any) | Stack[T](generics) | |
|---|---|---|
| 入れる時 | 何でも入る(型が失われる) | T だけ。違う型はコンパイルエラー |
| 取り出す時 | 型アサーションが必要・失敗でパニック | 型アサーション不要・型が保証される |
| コード量 | 1 つで済む | 1 つで済む(int/string/自作型すべてに適用) |
4. 直感・類推: ワンサイズの靴箱¶
特定の型に特化したコードは「int 専用の靴箱」「string 専用の靴箱」をそれぞれ作るようなものです。 ジェネリクスは「どんなサイズの靴も入る、ワンサイズの靴箱」です。
- 箱の作り方(Push/Pop のロジック)は 1 回だけ
- 使う時に「int 用」「string 用」とサイズ(型)を指定する
バックエンドでは、コレクション・キャッシュ・リポジトリ層など、データの型に依存しない 汎用ロジックで頻繁に使われます。
練習問題 1.5: ジェネリック関数 Filter を実装しよう¶
スライスから条件を満たす要素だけを残す、ジェネリック関数 Filter を実装してください。
仕様:
func Filter[T any](items []T, keep func(T) bool) []T
keepがtrueを返した要素だけを、元の順序のまま残したスライスを返すkeepが false の要素は含めない
ヒント: 空の result := []T{} を作って、keep が true の要素を append します。
// YOUR CODE HERE
// func Filter[T any](items []T, keep func(T) bool) []T を実装してください。
// (未実装のままチェックセルを実行すると「未回答」と表示されます)
func Filter[T any](items []T, keep func(T) bool) []T {
return nil
}
チェックのためのヘルパー¶
import "reflect"
import "fmt"
func mustEqual(got, want any, name string) {
if reflect.DeepEqual(got, want) {
fmt.Printf("✅ Passed: %s\n", name)
return
}
panic(fmt.Sprintf("❌ %s\n got = %v (%T)\n want = %v (%T)", name, got, got, want, want))
}
%%
evens := Filter([]int{1, 2, 3, 4, 5}, func(x int) bool { return x%2 == 0 })
if evens == nil {
fmt.Println("⚠️ 未回答: 練習問題を解いてから、このセルを再度実行してください")
} else {
mustEqual(evens, []int{2, 4}, "偶数だけ残す (int)")
shorts := Filter([]string{"cat", "elephant", "dog", "hippopotamus"}, func(s string) bool {
return len(s) <= 3
})
mustEqual(shorts, []string{"cat", "dog"}, "3 文字以下だけ残す (string)")
allEmpty := Filter([]int{}, func(x int) bool { return x > 0 })
mustEqual(allEmpty, []int{}, "空スライスは空を返す")
fmt.Println("🎉 すべてのチェックが通りました")
}
⚠️ 未回答: 練習問題を解いてから、このセルを再度実行してください
まとめ¶
- ジェネリクス(
[T any])で、型に依存しない型・関数を 1 回だけ書ける any+ 型アサーションは「取り出すまで型が分からない」リスクがある- ジェネリクスなら型がコンパイル時に保証される → 安全で読みやすい
答え合わせは 01.5-generics-solutions.ipynb で行ってください。
次は 01.6 で「SOLID とデザインパターン」を学びます。