ジェネリックを応援しよう

Go 1.18 ではジェネリックが導入されました。これにより、int文字列、およびカスタム型に対応する型パラメータを使用して関数やデータ構造を記述できるようになり、interface{} 型のアサーションが完全に不要になりました。

int文字列float64 に対して、ほぼ同じソート関数を 3 つ記述する必要がある場合、ジェネリクスを活用すれば、1 つだけ記述すれば済みます。

1. 学習内容


2. あるアルゴリズムエンジニアの実話

(1) 課題:型ごとにsort関数を書く必要があること

チャーリーは、int、float64、および文字列の3つの型をサポートするジェネリックなソートライブラリを実装する必要があります:

「Go言語のバージョン1.18以前は、ジェネリックがありませんでした。私は3つのまったく同じ関数を書きました。唯一の違いは型だけでした。新しい型を追加するたびに、コードをコピー&ペーストしなければなりませんでした。メンテナンスの負担はとてつもなく大きかったのです。」

GO
// 悪いコード:ジェネリックがない、各タイプごとにコピー&ペーストする
func SortInts(slice []int) {
    sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
}

func SortFloat64s(slice []float64) {
    sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
}

func SortStrings(slice []string) {
    sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
}
// 新しいタイプはどれも Ctrl+C / Ctrl+V

(2) Go 1.18 での解決策:ジェネリクス

GO
// 優れたコード:ある関数は、比較可能なすべての型に対応している
func Sort[T constraints.Ordered](slice []T) {
    sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
}

// 使用:型の自動推論
ints := []int{3, 1, 2}
Sort(ints)

floats := []float64{3.14, 1.41, 2.72}
Sort(floats)

strs := []文字列{"c", "a", "b"}
Sort(strs)
// 3つの関数は必要ありません!

(3) パフォーマンス:ジェネリクス導入前 vs. ジェネリクス導入後

次元 interface{} + 型アサーション ジェネリック
コードの量 型ごとに1コピー 1コピー
型安全性 ❌ 実行時のパニック ✅ コンパイル時のチェック
パフォーマンス パッキングおよびアンパッキングのオーバーヘッドを伴う ✅ オーバーヘッドなし
可読性 多数の型アサーション ✅ 明確

3. ジェネリック関数

▶ サンプル:基本的な汎用関数

GO
package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// ジェネリック関数:T は型パラメータ、any 制約(すべての型)
func Print[T any](value T) {
    fmt.Println(value)
}

// 複数の型パラメータ
func Pair[A, B any](a A, b B) (A, B) {
    return a, b
}

// 制約がソート可能な型であること
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    // 型パラメータを明示的に指定する
    Print[int](42)
    Print[string]("hello")

    // 型推論(コンパイラによる自動推論 T)
    Print(42)       // T = int
    Print("hello")  // T = string

    fmt.Println(Max(3, 5))           // T = int → 5
    fmt.Println(Max(3.14, 2.72))    // T = float64 → 3.14
    fmt.Println(Max("apple", "banana")) // T = string → "banana"

    a, b := Pair(1, "one")
    fmt.Printf("A=%v (type: %T), B=%v (type: %T)\n", a, a, b, b)
}
▶ 試してみよう

(1) 汎用関数の構文

GO
// 文法:func 関数名[型パラメータ 制約](パラメータ一覧) 戻り値
func Name[T Constraint](param T) T { ... }

// 型パラメータリストには [] を使う(角括弧ではない)
// 制約:any / comparable / カスタムインターフェース
// 戻り値として型パラメータを使用可能

4. 型制約

▶ サンプル:組み込み制約

GO
パッケージ main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// any:すべての型(interface{} に相当)
func Identity[T any](値 T) T {
    return 値
}

// comparable:比較可能な型(== と != をサポート)
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

// constraints.Ordered:順序付き型(< <= > >=)
func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Contains([]int{1, 2, 3}, 2))         // true
    fmt.Println(Contains([]文字列{"a", "b", "c"}, "d")) // false
    fmt.Println(Min(10, 20))                            // 10
}
▶ 試してみよう

▶ サンプル:カスタム制約

GO
package main

import "fmt"

// カスタム制約:インターフェース + 型集合
type Numeric interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
        ~float32 | ~float64
}

// ~int は基底型が int のすべての型(type MyInt int を含む)
// int は int 自身のみ

type Price float64

func Sum[T Numeric](values []T) T {
    var sum T
    for _, v := range values {
        sum += v
    }
    return sum
}

func main() {
    ints := []int{1, 2, 3, 4, 5}
    fmt.Printf("Sum(ints) = %d\n", Sum(ints)) // 15

    floats := []float64{1.5, 2.5, 3.0}
    fmt.Printf("Sum(floats) = %.1f\n", Sum(floats)) // 7.0

    prices := []Price{10.99, 20.99, 5.00}
    fmt.Printf("Sum(prices) = %.2f\n", Sum(prices)) // 36.98(基本型 float64)
}
▶ 試してみよう

(2) 制約レベル

制約 サポートされている操作 ソース
any すべての演算(制約なし) 組み込み
comparable == != 内蔵
constraints.Ordered < <= > >= golang.org/x/exp
constraints.Integer すべての整数型 golang.org/x/exp
constraints.Float すべての浮動小数点型 golang.org/x/exp
カスタム ユニオン型 `interface { ~int
💡 ヒント: anyinterface{} と同等であり、comparable は組み込みの制約です(インポートは不要)。constraints.Orderedconstraints.Integer などは golang.org/x/exp/constraints パッケージに含まれています。これは実験的な機能ですが、事実上の標準となっています。Go 1.21以降では、一部の制約が標準ライブラリに移行されています。


5. 汎用データ構造

▶ サンプル:ジェネリック Stack

GO
package main

import "fmt"

// Stack ジェネリックスタック
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

func (s *Stack[T]) Peek() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    return s.items[len(s.items)-1], true
}

func (s *Stack[T]) IsEmpty() bool {
    return len(s.items) == 0
}

func (s *Stack[T]) Size() int {
    return len(s.items)
}

func main() {
    // int スタック
    intStack := Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    intStack.Push(3)

    for !intStack.IsEmpty() {
        if val, ok := intStack.Pop(); ok {
            fmt.Printf("Popped: %d\n", val)
        }
    }

    // string スタック
    strStack := Stack[string]{}
    strStack.Push("hello")
    strStack.Push("world")
    fmt.Printf("Peek: %s\n", strStack.Peek()) // world
}
▶ 試してみよう

▶ サンプル:汎用セット

GO
パッケージ main

import "fmt"

// Set ジェネリックコレクション(comparable 制約)
type Set[T comparable] struct {
    アイテム map[T]struct{}
}

func NewSet[T comparable]() *Set[T] {
    return &Set[T]{アイテム: make(map[T]struct{})}
}

func (s *Set[T]) Add(item T) {
    s.アイテム[item] = struct{}{}
}

func (s *Set[T]) Remove(item T) {
    削除(s.アイテム, item)
}

func (s *Set[T]) Contains(item T) bool {
    _, ok := s.アイテム[item]
    return ok
}

func (s *Set[T]) Size() int {
    return len(s.アイテム)
}

func (s *Set[T]) Items() []T {
    result := make([]T, 0, len(s.アイテム))
    for item := range s.アイテム {
        result = append(result, item)
    }
    return result
}

// Union 和集合(メソッドレシーバに型パラメータは不可)
func Union[T comparable](a, b *Set[T]) *Set[T] {
    result := NewSet[T]()
    for _, item := range a.Items() {
        result.Add(item)
    }
    for _, item := range b.Items() {
        result.Add(item)
    }
    return result
}

// Intersection 積集合
func Intersection[T comparable](a, b *Set[T]) *Set[T] {
    result := NewSet[T]()
    for _, item := range a.Items() {
        if b.Contains(item) {
            result.Add(item)
        }
    }
    return result
}

func main() {
    set1 := NewSet[int]()
    set1.Add(1)
    set1.Add(2)
    set1.Add(3)

    set2 := NewSet[int]()
    set2.Add(3)
    set2.Add(4)
    set2.Add(5)

    fmt.Println("Set1:", set1.Items())
    fmt.Println("Set2:", set2.Items())
    fmt.Println("Union:", Union(set1, set2).Items())
    fmt.Println("Intersection:", Intersection(set1, set2).Items())
}
▶ 試してみよう
🔥 よくある間違い: ジェネリック構造体のメソッドでは、追加の型パラメータを宣言することはできません。利用可能なのは、その構造体自身の型パラメータのみです。追加の型パラメータが必要な場合は、関数を使用してください(例:Union[T comparable](a, b *Set[T]))。また、ジェネリック型は const の宣言で直接使用することはできません


6. 型推論とインスタンス化

GO
package main

import (
    "fmt"
    "strconv"
)
func Map[T, U any](input []T, fn func(T) U) []U {
    result := make([]U, len(input))
    for i, v := range input {
        result[i] = fn(v)
    }
    return result
}

func main() {
    // 型推論:T=int, U=string
    nums := []int{1, 2, 3, 4, 5}
    strs := Map(nums, strconv.Itoa)
    fmt.Println(strs) // ["1", "2", "3", "4", "5"]

    // 型パラメータを明示的に指定(推論失敗時)
    explicit := Map[int, string](nums, strconv.Itoa)
    fmt.Println(explicit)

    // ❌ 型パラメータは関数の引数/戻り値としてのみ使用可能、変数には不可
    // var list List[int]  ← 型のインスタンス化

    // 型のインスタンス化:特定の型でジェネリック型を生成
    var intStack Stack[int]
    intStack.Push(10)
}
100%
graph TB
    A[ジェネリック関数の定義<br/>func Max[T Ordered](a, b T) T] --> B{呼び出し Max(3, 5)}
    B --> C[コンパイラの推論 T = int]
    C --> D[インスタンス化 Max[int]]
    D --> E[int バージョン:func Max(a, b int) int]
    B --> F{呼び出し Max(3.14, 2.72)}
    F --> G[コンパイラの推論 T = float64]
    G --> H[インスタンス化 Max[float64]]
    H --> I[float64 バージョン:func Max(a, b float64) float64]

(3) ジェネリクスとinterface{}の比較

比較 interface{} + 型アサーション ジェネリクス
型安全性 ❌ 実行時のパニック ✅ コンパイル時のチェック
パフォーマンス ボックスイン/ボックスアウト(ヒープへのエスケープ) ✅ オーバーヘッドゼロ(コンパイル時の展開)
コードの量 型ごとに1つ ✅ 汎用コードを1つ
柔軟性 同じスライスに異なる型を格納できる ✅ 型はコンパイル時に決定される
複雑さ わかりやすい ⚠️ 複雑な構文

7. 完全な例:汎用ソートライブラリ

GO
// generic_sort.go
package main

import (
    "fmt"
    "sort"
    "golang.org/x/exp/constraints"
)

// ---------- ソート関数 ----------

// SortSlice は任意の順序付き型のスライスをソートする
func SortSlice[T constraints.Ordered](slice []T) {
    sort.Slice(slice, func(i, j int) bool {
        return slice[i] < slice[j]
    })
}

// ReverseSort 降順ソート
func ReverseSort[T constraints.Ordered](slice []T) {
    sort.Slice(slice, func(i, j int) bool {
        return slice[i] > slice[j]
    })
}

// ---------- 検索関数 ----------

// BinarySearch 二分探索(ソート済み)
func BinarySearch[T constraints.Ordered](slice []T, target T) (int, bool) {
    low, high := 0, len(slice)-1
    for low <= high {
        mid := low + (high-low)/2
        if slice[mid] == target {
            return mid, true
        } else if slice[mid] < target {
            low = mid + 1
        } else {
            high = mid - 1
        }
    }
    return -1, false
}

// ---------- 集約関数 ----------

// Filter フィルタリング
func Filter[T any](slice []T, predicate func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

// Reduce 集約
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    result := initial
    for _, v := range slice {
        result = fn(result, v)
    }
    return result
}

type Person struct {
    Name string
    Age  int
}

func main() {
    // 1. 整数ソート
    ints := []int{5, 2, 8, 1, 9, 3}
    SortSlice(ints)
    fmt.Printf("Sorted ints: %v\n", ints)

    // 2. 降順ソート
    ReverseSort(ints)
    fmt.Printf("Reverse: %v\n", ints)

    // 3. 文字列ソート
    strs := []string{"banana", "apple", "cherry", "date"}
    SortSlice(strs)
    fmt.Printf("Sorted strings: %v\n", strs)

    // 4. 二分探索
    idx, found := BinarySearch(ints, 5)
    fmt.Printf("BinarySearch 5: idx=%d, found=%v\n", idx, found)

    // 5. Filter
    evens := Filter(ints, func(n int) bool { return n%2 == 0 })
    fmt.Printf("Evens: %v\n", evens)

    // 6. Reduce
    sum := Reduce(ints, 0, func(acc, n int) int { return acc + n })
    fmt.Printf("Sum: %d\n", sum)

    // 7. カスタム型(Person は Ordered を実装していないため直接ソート不可)
    // カスタムソート関数が必要
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
    }

    // クロージャでカスタムソートを実装
    sort.Slice(people, func(i, j int) bool {
        return people[i].Age < people[j].Age
    })
    fmt.Printf("Sorted by age: %v\n", people)
}
💡 ヒント: sort.Slice 自体はジェネリック関数ではありません。これは any スライス ([]any) を受け取り、リフレクションを介して動作します。しかし、クロージャと併用すると、ジェネリックとうまく連携します。真のジェネリックソートは、Go 1.21で導入された新機能であるslices.Sort (golang.org/x/exp/slices)によるもので、これは完全にジェネリックを使用して実装されています。


❓ よくある質問

Q ジェネリクスはいつ導入されましたか?
A Go 1.18(2022年3月リリース)です。Goチームは10年をかけてジェネリクスのソリューションを設計し、最終的にはC++のテンプレートベースやJavaの消去(イレイジャー)ベースのアプローチではなく、「型パラメータ」というアプローチを採用しました。これはコンパイル時に展開されるため、実行時のオーバーヘッドはゼロです。
Q 型パラメータの構文はどのようなものですか?
A func Name[T Constraint](param T) T。型パラメータは角括弧 [](山括弧ではありません)を使用して宣言します。複数のパラメータを指定することも可能です:func Map[T, U any](input []T, fn func(T) U) []U
Q 制約はどのように記述しますか?
A interface { set of types }。組み込みの制約:any(任意の型)、comparable(比較可能)。サードパーティ製:constraints.Ordered(ソート済み)。カスタム:interface { ~int | ~string }
Q anyinterface{} の違いは何ですか?
A anyinterface{} (type any = interface{}) の別名であり、両者は完全に同等です。Go 1.18では、ジェネリックが導入されると同時に、anyが型エイリアスとして追加されました。ジェネリック制約ではanyを、通常のコードではinterface{}を使用することを推奨します。
Q 型推論はどのように機能しますか?
A コンパイラは、関数の引数に基づいて型パラメータを自動的に推論します。たとえば、Max(3, 5) → では T=int と推論されます。推論に失敗した場合や、型を明示的に指定したい場合は、Max[int](3, 5) と記述できます。型パラメータは戻り値の型からは推論できません。少なくとも1つの引数に型パラメータが含まれている必要があります。
Q メソッド内でジェネリックを使用できますか?
A メソッドには追加の型パラメータを指定できません。構造体の型パラメータのみ使用可能です。したがって、func (s *Stack[T]) Push(item T)は有効ですが、func (s *Stack[T]) Convert[U any]() Uは無効です。追加の型パラメータが必要な場合は、メソッドではなく通常の関数を使用してください。
Q ジェネリックのパフォーマンスはどのように比較されますか?
A 実行時のオーバーヘッドはゼロです。Goのジェネリックは、コンパイル時に各型パラメータの組み合わせごとに固有の実装を生成します(モノモルフィゼーション)。したがって、Stack[int]Stack[string]は実行時には完全に異なる型であり、ボクシングやアンボクシングは発生しません。唯一のコストは、コンパイル時間のわずかな増加とバイナリサイズの拡大です。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐)slice 内の target のインデックスを返す汎用関数 Find[T comparable](slice []T, target T) int を実装してください。target が存在しない場合は -1 を返してください。intstringfloat64 の 3 つの型について、正しく動作することを確認してください。

  2. 上級問題(難易度 ⭐⭐):汎用的な Queue[T any](先入れ先出しキュー)を実装してください。要件:(1) enqueue、dequeue、peek、isEmpty メソッドを実装すること;(2) 任意の型に対応すること;(3) 頻繁なサイズ変更を避けるため、リングバッファを実装すること;(4) -race を使用して並行処理の安全性を検証すること。

  3. 課題(難易度 ⭐⭐⭐):汎用的な 並行処理に安全なキャッシュ Cache[K comparable, V any] を実装してください。要件:(1) Get/Set/Delete/Clear メソッド;(2) RWMutex による保護;(3) TTL による有効期限切れ処理;(4) OnEvicted コールバックのサポート(キーが削除されたり有効期限が切れたりした際に呼び出される);(5) ジェネリックを使用して、キーの型が比較可能であることを保証すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%