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
}

ジェネリクスを使った解決策:

例

// 1つの関数で複数の型を処理
func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

ジェネリクス構文の詳細

型パラメータの宣言

ジェネリック関数と型は型パラメータリストによって宣言します。構文は次のとおりです。[型パラメータ 制約]。

例

// 基本構文
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

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

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

カスタム制約

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)

2. 複雑な制約

型とメソッドの要件を組み合わせます。

例

// 型が数値であり、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]
}

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
}

ジェネリック型

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
}

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]
}

型推論

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)
}

実践演習

練習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] となるはずです
}

練習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)
}

よくある質問と注意点

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
}
その他の拡張