Go/ジェネリクス
< Go
ジェネリクス
編集Go言語のジェネリクスは、Go 1.18で導入された機能です。従来のインターフェースベースの実装と、新しい型パラメータによる実装の両方がサポートされています。
概要
編集ジェネリクスを使用することで、型安全性を保ちながら汎用的なコードを記述できます。主な利点は:
- コンパイル時の型チェック
- 型アサーションの必要性の低減
- コードの再利用性の向上
実装方法の比較
編集interfaceを使った従来の実装
編集- メリット
-
- Go 1.18以前から使用可能
- シンプルな実装
- 任意の型を扱える柔軟性
- デメリット
-
- 型の安全性が低い
- 実行時の型アサーションが必要
- パフォーマンスのオーバーヘッド
- 非ジェネリクス版スタック
package main import ( "fmt" ) // スタックの構造体 type Stack struct { data []interface{} } // スタックに要素を追加 func (s *Stack) Push(item interface{}) { s.data = append(s.data, item) } // スタックから要素を取り出す func (s *Stack) Pop() interface{} { item := s.data[len(s.data)-1] s.data = s.data[:len(s.data)-1] return item } // スタックの先頭の要素を取得(削除はしない) func (s *Stack) Peek() interface{} { return s.data[len(s.data)-1] } // スタックが空かどうかをチェックするメソッド func (s *Stack) IsEmpty() bool { return len(s.data) == 0 } func main() { // 空のスタックを作成 stack := Stack{} // スタックに値を追加 stack.Push(10) stack.Push("Hello") stack.Push(3.14) // スタックの内容を表示 for !stack.IsEmpty() { item := stack.Pop() fmt.Println("Popped:", item) } }
- 実行結果
Popped: 3.14 Popped: Hello Popped: 10
型パラメータを使った実装
編集- メリット
-
- コンパイル時の型チェック
- 型安全性の向上
- 実行時のオーバーヘッド削減
- デメリット
-
- Go 1.18以降が必要
- コードがやや複雑になる可能性
- ジェネリクス版スタック
package main import ( "fmt" ) // ジェネリクス化されたスタックの定義 type Stack[T any] []T // スタックに要素を追加するメソッド func (s *Stack[T]) Push(item T) { *s = append(*s, item) } // スタックから要素を取り出すメソッド func (s *Stack[T]) Pop() T { item := (*s)[len(*s)-1] *s = (*s)[:len(*s)-1] return item } // スタックが空かどうかをチェックするメソッド func (s *Stack[T]) IsEmpty() bool { return len(*s) == 0 } func main() { // int型のスタックを作成 var intStack Stack[int] // スタックに値を追加 intStack.Push(1) intStack.Push(2) intStack.Push(3) // スタックから値を取り出して表示 for !intStack.IsEmpty() { item := intStack.Pop() fmt.Println(item) } // 文字列型のスタックを作成 var stringStack Stack[string] // スタックに値を追加 stringStack.Push("apple") stringStack.Push("banana") stringStack.Push("cherry") // スタックから値を取り出して表示 for !stringStack.IsEmpty() { item := stringStack.Pop() fmt.Println(item) } }
- 実行結果
3 2 1 cherry banana apple
型制約
編集基本的な型制約
編集Go 1.18では、型パラメータに対して以下の制約を指定できます:
any
: すべての型と一致する制約(interface{}
のエイリアス)comparable
:==
や!=
による比較が可能な型- カスタムインターフェース: 特定のメソッドを要求する制約
- 組み込み制約の例:
constraints.Ordered
: 順序付け可能な型(数値型や文字列型)constraints.Integer
: 整数型constraints.Float
: 浮動小数点型
インターフェースによる型制約の例
編集- 型制約の実装
package main import "fmt" // Stringerインターフェースを満たす型制約の例 type Bin int func (b Bin) String() string { return fmt.Sprintf("%#b", int(b)) } // fmt.Stringerインターフェースを型制約として使用 func Print[T fmt.Stringer](s []T) { for _, v := range s { fmt.Print(v, "\n") } } // カスタム型制約の定義例 type Number interface { ~int | ~int32 | ~int64 | ~float32 | ~float64 } // Number制約を使用した総和計算関数 func Sum[T Number](values []T) T { var sum T for _, v := range values { sum += v } return sum } func main() { Print([]Bin{0b1101, 0b1010_0101}) numbers := []int{1, 2, 3, 4, 5} fmt.Printf("Sum: %v\n", Sum(numbers)) }
- 実行結果
0b1101 0b10100101 Sum: 15
推奨プラクティス
編集- 可能な限り具体的な型制約を使用する
- インターフェースを活用して意味のある制約を定義する
- 型パラメータの命名は意図が分かりやすいものにする
- 必要以上に複雑な型制約は避ける
- パフォーマンスを考慮する場合は、型制約を適切に選択する
関連項目
編集- インターフェース
- 型アサーション
- コンパイル時型チェック
- constraints パッケージ