Go言語ジェネリクス
ジェネリクスはGo言語の1.18バージョンで導入された重要な機能であり、開発者がより柔軟で再利用可能なコードを書くことを可能にします。
ジェネリクスは、主に以下の2つの中核概念によって実現されます:
- 型パラメータ(Type Parameters):関数や型の定義において、1つ以上の型をパラメータとして使用することを可能にします。
- 型制約(Type Constraints):型パラメータが満たすべき条件を指定し、関数内でこれらの型を安全に操作できることを保証します。
| 概念 | 役割 | 例 |
|---|---|---|
| 型パラメータ | 関数名や型名の後に宣言し、未確定の型を表します。 | [T any] |
| 型制約 | 型パラメータが満たすべき条件(サポートする演算子やメソッドなど)を定義します。 | int,float64,comparable,constraints.Ordered,any |
any |
型パラメータを制約して任意の型にします。 | [T any] |
comparable |
型パラメータを制約して比較可能な型にします。 | [K comparable] |
ジェネリクス(Generics)は、特定のデータ型に依存しないコードを書くことを可能にします。
ジェネリクスが導入される前は、異なる型のデータを扱う場合、通常は型ごとに同じような関数を書く必要がありました。
従来方式の限界:
例
// int型を処理する関数
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
// float64型を処理する関数
func MaxFloat(a, b float64) float64 {
if a > b {
return a
}
return b
}
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
// float64型を処理する関数
func MaxFloat(a, b float64) float64 {
if a > b {
return a
}
return b
}
ジェネリクスを使った解決策:
例
// 1つの関数で複数の型を処理
func Max[T comparable](a, b T) T {
if a > b {
return a
}
return b
}
func Max[T comparable](a, b T) T {
if a > b {
return a
}
return b
}
ジェネリクス構文の詳細
型パラメータの宣言
ジェネリック関数と型は型パラメータリストによって宣言します。構文は次のとおりです。[型パラメータ 制約]。
例
// 基本構文
func関数名[T 制約](パラメータ T)戻り値の型{
// 関数本体
}
type型名[T 制約] struct {
// 構造体のフィールド
}
func関数名[T 制約](パラメータ T)戻り値の型{
// 関数本体
}
type型名[T 制約] struct {
// 構造体のフィールド
}
型パラメータの命名規則
- 通常は大文字を使用します:
T、K、V、E等 TT:Type(型)を表します。KK:Key(キー)を表します。VV:Value(値)を表します。EE:Element(要素)を表します。
制約(Constraints)
制約は型パラメータが満たすべき条件を定義し、ジェネリクスの中核概念です。
組み込み制約
1. anyany制約は
any空インターフェースinterface{}のエイリアスであり、任意の型が可能であることを示します。
例
func PrintAny[T any](value T) {
fmt.Printf("Value: %v, Type: %T\n", value, value)
}
// 使用例
PrintAny(42) // Value: 42, Type: int
PrintAny("hello") // Value: hello, Type: string
PrintAny(3.14) // Value: 3.14, Type: float64
fmt.Printf("Value: %v, Type: %T\n", value, value)
}
// 使用例
PrintAny(42) // Value: 42, Type: int
PrintAny("hello") // Value: hello, Type: string
PrintAny(3.14) // Value: 3.14, Type: float64
2. comparablecomparable制約は
comparable型が==と!=演算子をサポートすることを示します。
例
func FindIndex[T comparable](slice []T, target T) int {
for i, v := range slice {
if v == target {
return i
}
}
return -1
}
// 使用例
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(FindIndex(numbers, 3)) // 出力: 2
names := []string{"Alice", "Bob", "Charlie"}
fmt.Println(FindIndex(names, "Bob")) // 出力: 1
for i, v := range slice {
if v == target {
return i
}
}
return -1
}
// 使用例
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(FindIndex(numbers, 3)) // 出力: 2
names := []string{"Alice", "Bob", "Charlie"}
fmt.Println(FindIndex(names, "Bob")) // 出力: 1
3. ユニオン制約(Union Constraints)
`|` 演算子を使用して|複数の型を組み合わせます。
例
// 数値型の制約
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func Add[T Number](a, b T) T {
return a + b
}
// 使用例
fmt.Println(Add(10, 20)) // 出力: 30
fmt.Println(Add(3.14, 2.71)) // 出力: 5.85
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func Add[T Number](a, b T) T {
return a + b
}
// 使用例
fmt.Println(Add(10, 20)) // 出力: 30
fmt.Println(Add(3.14, 2.71)) // 出力: 5.85
カスタム制約
1. メソッド制約
特定のメソッドを必要とする制約を定義します。
例
// Stringer制約を定義
type Stringer interface {
String() string
}
func PrintString[T Stringer](value T) {
fmt.Println(value.String())
}
// カスタム型を実装
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
// 使用例
person := Person{Name: "Alice", Age: 25}
PrintString(person) // 出力: Alice (25 years old)
type Stringer interface {
String() string
}
func PrintString[T Stringer](value T) {
fmt.Println(value.String())
}
// カスタム型を実装
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
// 使用例
person := Person{Name: "Alice", Age: 25}
PrintString(person) // 出力: Alice (25 years old)
2. 複雑な制約
型とメソッドの要件を組み合わせます。
例
// 型が数値であり、String() メソッドを実装していることを要求
type NumericStringer interface {
Number
String() string
}
type NumericStringer interface {
Number
String() string
}
ジェネリック関数の実践
1. 汎用ユーティリティ関数
例
// 2つの値を交換
func Swap[T any](a, b T) (T, T) {
return b, a
}
// スライスが要素を含むかどうかを判定
func Contains[T comparable](slice []T, target T) bool {
for _, item := range slice {
if item == target {
return true
}
}
return false
}
// 重複除去関数
func Unique[T comparable](slice []T) []T {
seen := make(map[T]bool)
result := []T{}
for _, item := range slice {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
// 使用例
func main() {
// Swapの例
a, b := 10, 20
a, b = Swap(a, b)
fmt.Printf("a=%d, b=%d\n", a, b) // 出力: a=20, b=10
// Containsの例
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(Contains(numbers, 3)) // 出力: true
// Uniqueの例
duplicates := []int{1, 2, 2, 3, 4, 4, 5}
unique := Unique(duplicates)
fmt.Println(unique) // 出力: [1 2 3 4 5]
}
func Swap[T any](a, b T) (T, T) {
return b, a
}
// スライスが要素を含むかどうかを判定
func Contains[T comparable](slice []T, target T) bool {
for _, item := range slice {
if item == target {
return true
}
}
return false
}
// 重複除去関数
func Unique[T comparable](slice []T) []T {
seen := make(map[T]bool)
result := []T{}
for _, item := range slice {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
// 使用例
func main() {
// Swapの例
a, b := 10, 20
a, b = Swap(a, b)
fmt.Printf("a=%d, b=%d\n", a, b) // 出力: a=20, b=10
// Containsの例
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(Contains(numbers, 3)) // 出力: true
// Uniqueの例
duplicates := []int{1, 2, 2, 3, 4, 4, 5}
unique := Unique(duplicates)
fmt.Println(unique) // 出力: [1 2 3 4 5]
}
2. 数学演算関数
例
// スライスの最大値を求める
func Max[T Number](slice []T) T {
if len(slice) == 0 {
var zero T
return zero
}
max := slice[0]
for _, value := range slice[1:] {
if value > max {
max = value
}
}
return max
}
// スライスの最小値を求める
func Min[T Number](slice []T) T {
if len(slice) == 0 {
var zero T
return zero
}
min := slice[0]
for _, value := range slice[1:] {
if value < min {
min = value
}
}
return min
}
// スライスの平均値を求める
func Average[T Number](slice []T) float64 {
if len(slice) == 0 {
return 0
}
var sum T
for _, value := range slice {
sum += value
}
return float64(sum) / float64(len(slice))
}
// 使用例
func main() {
ints := []int{1, 5, 3, 9, 2}
floats := []float64{1.1, 5.5, 3.3, 9.9, 2.2}
fmt.Printf("Max int: %d\n", Max(ints)) // 出力: 9
fmt.Printf("Min float: %.1f\n", Min(floats)) // 出力: 1.1
fmt.Printf("Average: %.2f\n", Average(floats)) // 出力: 4.40
}
func Max[T Number](slice []T) T {
if len(slice) == 0 {
var zero T
return zero
}
max := slice[0]
for _, value := range slice[1:] {
if value > max {
max = value
}
}
return max
}
// スライスの最小値を求める
func Min[T Number](slice []T) T {
if len(slice) == 0 {
var zero T
return zero
}
min := slice[0]
for _, value := range slice[1:] {
if value < min {
min = value
}
}
return min
}
// スライスの平均値を求める
func Average[T Number](slice []T) float64 {
if len(slice) == 0 {
return 0
}
var sum T
for _, value := range slice {
sum += value
}
return float64(sum) / float64(len(slice))
}
// 使用例
func main() {
ints := []int{1, 5, 3, 9, 2}
floats := []float64{1.1, 5.5, 3.3, 9.9, 2.2}
fmt.Printf("Max int: %d\n", Max(ints)) // 出力: 9
fmt.Printf("Min float: %.1f\n", Min(floats)) // 出力: 1.1
fmt.Printf("Average: %.2f\n", Average(floats)) // 出力: 4.40
}
ジェネリック型
1. ジェネリック構造体
例
// ジェネリックスタックの実装
type Stack[T any] struct {
elements []T
}
// プッシュ
func (s *Stack[T]) Push(value T) {
s.elements = append(s.elements, value)
}
// ポップ
func (s *Stack[T]) Pop() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
lastIndex := len(s.elements) - 1
value := s.elements[lastIndex]
s.elements = s.elements[:lastIndex]
return value, true
}
// スタックの先頭要素を確認
func (s *Stack[T]) Peek() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
return s.elements[len(s.elements)-1], true
}
// スタックが空かどうかを判定
func (s *Stack[T]) IsEmpty() bool {
return len(s.elements) == 0
}
// 使用例
func main() {
// 整数スタック
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
fmt.Println(intStack.Pop()) // 出力: 3 true
// 文字列スタック
stringStack := Stack[string]{}
stringStack.Push("hello")
stringStack.Push("world")
fmt.Println(stringStack.Pop()) // 出力: world true
}
type Stack[T any] struct {
elements []T
}
// プッシュ
func (s *Stack[T]) Push(value T) {
s.elements = append(s.elements, value)
}
// ポップ
func (s *Stack[T]) Pop() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
lastIndex := len(s.elements) - 1
value := s.elements[lastIndex]
s.elements = s.elements[:lastIndex]
return value, true
}
// スタックの先頭要素を確認
func (s *Stack[T]) Peek() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
return s.elements[len(s.elements)-1], true
}
// スタックが空かどうかを判定
func (s *Stack[T]) IsEmpty() bool {
return len(s.elements) == 0
}
// 使用例
func main() {
// 整数スタック
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
fmt.Println(intStack.Pop()) // 出力: 3 true
// 文字列スタック
stringStack := Stack[string]{}
stringStack.Push("hello")
stringStack.Push("world")
fmt.Println(stringStack.Pop()) // 出力: world true
}
2. ジェネリックマップ(Map)
例
// スレッドセーフなジェネリックマップ
type SafeMap[K comparable, V any] struct {
data map[K]V
mutex sync.RWMutex
}
// 新しいSafeMapを作成
func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
return &SafeMap[K, V]{
data: make(map[K]V),
}
}
// キーと値のペアを設定
func (m *SafeMap[K, V]) Set(key K, value V) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.data[key] = value
}
// 値を取得
func (m *SafeMap[K, V]) Get(key K) (V, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
value, exists := m.data[key]
return value, exists
}
// キーを削除
func (m *SafeMap[K, V]) Delete(key K) {
m.mutex.Lock()
defer m.mutex.Unlock()
delete(m.data, key)
}
// すべてのキーを取得
func (m *SafeMap[K, V]) Keys() []K {
m.mutex.RLock()
defer m.mutex.RUnlock()
keys := make([]K, 0, len(m.data))
for key := range m.data {
keys = append(keys, key)
}
return keys
}
// 使用例
func main() {
// 文字列から整数へのマップを作成
scores := NewSafeMap[string, int]()
scores.Set("Alice", 95)
scores.Set("Bob", 87)
if score, exists := scores.Get("Alice"); exists {
fmt.Printf("Alice's score: %d\n", score) // 出力: Alice's score: 95
}
fmt.Println("Keys:", scores.Keys()) // 出力: Keys: [Alice Bob]
}
type SafeMap[K comparable, V any] struct {
data map[K]V
mutex sync.RWMutex
}
// 新しいSafeMapを作成
func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
return &SafeMap[K, V]{
data: make(map[K]V),
}
}
// キーと値のペアを設定
func (m *SafeMap[K, V]) Set(key K, value V) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.data[key] = value
}
// 値を取得
func (m *SafeMap[K, V]) Get(key K) (V, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
value, exists := m.data[key]
return value, exists
}
// キーを削除
func (m *SafeMap[K, V]) Delete(key K) {
m.mutex.Lock()
defer m.mutex.Unlock()
delete(m.data, key)
}
// すべてのキーを取得
func (m *SafeMap[K, V]) Keys() []K {
m.mutex.RLock()
defer m.mutex.RUnlock()
keys := make([]K, 0, len(m.data))
for key := range m.data {
keys = append(keys, key)
}
return keys
}
// 使用例
func main() {
// 文字列から整数へのマップを作成
scores := NewSafeMap[string, int]()
scores.Set("Alice", 95)
scores.Set("Bob", 87)
if score, exists := scores.Get("Alice"); exists {
fmt.Printf("Alice's score: %d\n", score) // 出力: Alice's score: 95
}
fmt.Println("Keys:", scores.Keys()) // 出力: Keys: [Alice Bob]
}
型推論
Goコンパイラは型パラメータを自動的に推論できるため、コードをより簡潔にできます。
例
// 型を明示的に指定する必要はありません
func main() {
// 型推論の例
fmt.Println(Max([]int{1, 2, 3})) // コンパイラは T を int と推論
fmt.Println(Max([]float64{1.1, 2.2})) // コンパイラは T を float64 と推論
// 型を明示的に指定する場合(必要なとき)
var result int = Max[int]([]int{1, 2, 3})
fmt.Println(result)
}
func main() {
// 型推論の例
fmt.Println(Max([]int{1, 2, 3})) // コンパイラは T を int と推論
fmt.Println(Max([]float64{1.1, 2.2})) // コンパイラは T を float64 と推論
// 型を明示的に指定する場合(必要なとき)
var result int = Max[int]([]int{1, 2, 3})
fmt.Println(result)
}
実践演習
練習1: ジェネリックフィルタの実装
`Filter` 関数をFilter実装し、条件に基づいてスライスの要素をフィルタリングします。
例
// ここに実装コードを記述
func Filter[T any](slice []T, predicate func(T) bool) []T {
// フィルタリングロジックを実装
}
// テストコード
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
even := Filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(even) // 出力: [2 4 6] となるはずです
}
func Filter[T any](slice []T, predicate func(T) bool) []T {
// フィルタリングロジックを実装
}
// テストコード
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
even := Filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(even) // 出力: [2 4 6] となるはずです
}
練習2: ジェネリックマッピング関数の実装
`Map` 関数をMap実装し、スライス内の各要素を別の型に変換します。
例
// ここに実装コードを記述
func Map[T any, U any](slice []T, mapper func(T) U) []U {
// マッピングロジックを実装
}
// テストコード
func main() {
numbers := []int{1, 2, 3, 4, 5}
strings := Map(numbers, func(n int) string {
return fmt.Sprintf("Number: %d", n)
})
fmt.Println(strings)
}
func Map[T any, U any](slice []T, mapper func(T) U) []U {
// マッピングロジックを実装
}
// テストコード
func main() {
numbers := []int{1, 2, 3, 4, 5}
strings := Map(numbers, func(n int) string {
return fmt.Sprintf("Number: %d", n)
})
fmt.Println(strings)
}
よくある質問と注意点
1. パフォーマンスに関する考慮事項
ジェネリクスはコンパイル時に型の特殊化が行われるため、実行時パフォーマンスは特定の型に特化した手書きコードと同等です。
2. 型制約の選択
- 使用
any時は最も柔軟ですが、機能は制限されます - 使用
comparable等価比較をサポート - union制約を使用して利用可能な具体的な型を制限する
3. エラーハンドリング
例
// 良いエラー処理のプラクティス
func SafeMax[T Number](slice []T) (T, error) {
if len(slice) == 0 {
var zero T
return zero, errors.New("slice is empty")
}
return Max(slice), nil
}
func SafeMax[T Number](slice []T) (T, error) {
if len(slice) == 0 {
var zero T
return zero, errors.New("slice is empty")
}
return Max(slice), nil
}