skeleton/stepX/main.go
を編集して実装しますsolution/stepX/main.go
と比較して理解を深めます各ステップのコードを修正したら、以下のコマンドで動作を確認してください。
go run skeleton/stepX/main.go
型パラメータ という新しい概念を理解し、同一のロジックを複数の型で再利用する方法を学びます。
Go 1.18以前は、複数の型に対して同じ処理を書く場合、以下のような選択肢しかありませんでした。
interface{}
(現在のany
)を使う(型安全性の喪失)現在の skeleton/step1/main.go
は any
を使った実装例です。この方法には以下の問題があります。
// 問題1: 型情報が失われる
func (s Slice) Filter(f func(any) bool) Slice // any型として扱う
// 問題2: 使う側で型アサーションが必要
evens := ints.Filter(func(i any) bool {
return (i.(int))%2 == 0 // .(int) で型アサーション
})
// 問題3: 実行時にパニックのリスク
// もし間違った型でアクセスしたら実行時エラー
型パラメータは「型を後から決める」仕組みです。
// [T any] が型パラメータ
// T は「何かの型」を表すプレースホルダー
type Slice[T any] []T
// 使うときに具体的な型を指定
var ints Slice[int] // T = int
var strings Slice[string] // T = string
重要な概念
T
は型変数(Type Variable)と呼ばれるany
は型制約(Type Constraint)— この場合「どんな型でもOK」func Map[A, B any](...)
Go のコンパイラは多くの場合、型パラメータを自動で推論できます。
// 明示的に型を指定
ints := Slice[int]{1, 2, 3}
// 型推論により省略可能
ints := Slice{1, 2, 3} // 要素から int と推論
このステップの内容を踏まえて、skeleton/step1/main.go
をジェネリクスを用いて修正してください。
Interface を型制約として使用し、型パラメータに「条件」を付ける方法を学びます。
Step 1 では any
を使いましたが、これは「どんな型でもOK」という意味です。しかし、実際のコードでは「特定のメソッドを持つ型」という条件を付けたい場合があります。
// 現在の問題:T は any なので String() メソッドが保証されない
type Container[T any] struct {
items []T
}
func (c *Container[T]) PrintAll() {
for _, item := range c.items {
fmt.Println(item.String()) // ❌ コンパイルエラー!
}
}
Interface を型制約として使うことで、「この条件を満たす型のみ」を指定できます。
// T は fmt.Stringer を実装した型のみ
type Container[T fmt.Stringer] struct {
items []T
}
重要な違い:型の統一性
// 従来の方法:異なる型を混在できる
func PrintAll(items []fmt.Stringer) {
// items = []fmt.Stringer{Person{}, Product{}} // 異なる型OK
}
// ジェネリクスの型制約:同一の型で統一
type Container[T fmt.Stringer] struct {
items []T // すべて同じ具体的な型T
}
// Container[Person] と Container[Product] は別の型
型制約のメリット
// Interface を型制約として使うと...
container := Container[Person]{}
container.Add(Person{"Alice", 30}) // ✅ OK
container.Add(Product{"Book", 10}) // ❌ コンパイルエラー(型が違う)
// 通常の interface 引数だと...
items := []fmt.Stringer{}
items = append(items, Person{"Alice", 30}) // ✅ OK
items = append(items, Product{"Book", 10}) // ✅ OK(混在可能)
このステップの内容を踏まえて、skeleton/step2/main.go
を修正してください。
複数の制約を組み合わせた高度な型制約パターンを理解し、ポインタ専用の制約を正しく表現できるようになります。
JSON のアンマーシャル処理を汎用化したい場合、値そのもの (T
) を返しつつ *T
にだけ定義されたメソッドを呼び出さなければなりません。json.Unmarshal
はポインタを受け取るため、型制約で「*T
かつ json.Unmarshaler
」を厳密に指定する必要があります。
var user User
json.Unmarshal(data, &user) // ポインタが必須
user, err := Unmarshal[User](data) // ジェネリクスで値型として受け取りたい
以下のように複数の型制約を同時に満たす型を指定します。
type Unmershaller[T any] interface {
*T
json.Unmarshaler
}
これは「*T
かつ json.Unmarshaler
を実装した型」を意味します。
func Unmarshal[T any, PT Unmershaller[T]](data []byte) (T, error) {
var v T
err := PT(&v).UnmarshalJSON(data)
return v, err
}
T
: 呼び出し側へ返したい値型(例:User
)PT
: *T
と互換性があり、json.Unmarshaler
を実装した型Go 1.20 以降では PT
を省略でき、Unmarshal[User](data)
のようにシンプルに呼び出せます。コンパイラが Unmershaller[User]
を満たす型として *User
を推論します。
skeleton/step3/main.go
の Unmershaller
は json.Unmarshaler
しか制約として指定していません。そのため、コンパイラは PT
と *T
の関係を理解できず、以下のエラーが発生します:
cannot convert &v (value of type *T) to type PT
このステップの内容を踏まえて、skeleton/step3/main.go
を修正してください。
*T
かつ json.Unmarshaler
)の意味を説明できるT
と PT
)が必要か理解しているPT(&v)
のキャストが必要な理由を説明できるStep 1: 型パラメータの基礎
Step 2: Interface による型制約
Step 3: 複雑な型制約
slices
, maps
パッケージなど)