コンテンツにスキップ

Go/ジェネリクス

出典: フリー教科書『ウィキブックス(Wikibooks)』
< 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 パッケージ

脚註

[編集]