ジェネリックを応援しよう
Go 1.18 ではジェネリックが導入されました。これにより、
int、文字列、およびカスタム型に対応する型パラメータを使用して関数やデータ構造を記述できるようになり、interface{}型のアサーションが完全に不要になりました。
int、文字列、float64 に対して、ほぼ同じソート関数を 3 つ記述する必要がある場合、ジェネリクスを活用すれば、1 つだけ記述すれば済みます。
1. 学習内容
- 汎用関数の構文
- 一般的なデータ構造
anyおよびcomparableの制約constraints: カスタム制約- 型推論
- ジェネリックと
interface{}の比較
2. あるアルゴリズムエンジニアの実話
(1) 課題:型ごとにsort関数を書く必要があること
チャーリーは、int、float64、および文字列の3つの型をサポートするジェネリックなソートライブラリを実装する必要があります:
「Go言語のバージョン1.18以前は、ジェネリックがありませんでした。私は3つのまったく同じ関数を書きました。唯一の違いは型だけでした。新しい型を追加するたびに、コードをコピー&ペーストしなければなりませんでした。メンテナンスの負担はとてつもなく大きかったのです。」
// 悪いコード:ジェネリックがない、各タイプごとにコピー&ペーストする
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 での解決策:ジェネリクス
// 優れたコード:ある関数は、比較可能なすべての型に対応している
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. ジェネリック関数
▶ サンプル:基本的な汎用関数
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) 汎用関数の構文
// 文法:func 関数名[型パラメータ 制約](パラメータ一覧) 戻り値
func Name[T Constraint](param T) T { ... }
// 型パラメータリストには [] を使う(角括弧ではない)
// 制約:any / comparable / カスタムインターフェース
// 戻り値として型パラメータを使用可能
4. 型制約
▶ サンプル:組み込み制約
パッケージ 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
}
▶ サンプル:カスタム制約
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 |
any は interface{} と同等であり、comparable は組み込みの制約です(インポートは不要)。constraints.Ordered、constraints.Integer などは golang.org/x/exp/constraints パッケージに含まれています。これは実験的な機能ですが、事実上の標準となっています。Go 1.21以降では、一部の制約が標準ライブラリに移行されています。
5. 汎用データ構造
▶ サンプル:ジェネリック Stack
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
}
▶ サンプル:汎用セット
パッケージ 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. 型推論とインスタンス化
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)
}
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. 完全な例:汎用ソートライブラリ
// 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)によるもので、これは完全にジェネリックを使用して実装されています。
❓ よくある質問
func Name[T Constraint](param T) T。型パラメータは角括弧 [](山括弧ではありません)を使用して宣言します。複数のパラメータを指定することも可能です:func Map[T, U any](input []T, fn func(T) U) []U。interface { set of types }。組み込みの制約:any(任意の型)、comparable(比較可能)。サードパーティ製:constraints.Ordered(ソート済み)。カスタム:interface { ~int | ~string }。any と interface{} の違いは何ですか?any は interface{} (type any = interface{}) の別名であり、両者は完全に同等です。Go 1.18では、ジェネリックが導入されると同時に、anyが型エイリアスとして追加されました。ジェネリック制約ではanyを、通常のコードではinterface{}を使用することを推奨します。Max(3, 5) → では T=int と推論されます。推論に失敗した場合や、型を明示的に指定したい場合は、Max[int](3, 5) と記述できます。型パラメータは戻り値の型からは推論できません。少なくとも1つの引数に型パラメータが含まれている必要があります。func (s *Stack[T]) Push(item T)は有効ですが、func (s *Stack[T]) Convert[U any]() Uは無効です。追加の型パラメータが必要な場合は、メソッドではなく通常の関数を使用してください。Stack[int]とStack[string]は実行時には完全に異なる型であり、ボクシングやアンボクシングは発生しません。唯一のコストは、コンパイル時間のわずかな増加とバイナリサイズの拡大です。📖 まとめ
- 汎用関数:
func Name[T Constraint](param T) T - タイプ制約:
any/comparable/Ordered/ カスタム - カスタム制約:
interface { ~int | ~string } - 型推論:コンパイラによって自動的に推論されるか、明示的に指定される
- 汎用データ構造:
Stack[T]/Set[T]/List[T] - メソッドには追加の型パラメータを指定することはできません。
- 実行時のオーバーヘッドがゼロ(コンパイル時の展開)
📝 練習問題
-
基本問題(難易度 ⭐):
slice内のtargetのインデックスを返す汎用関数Find[T comparable](slice []T, target T) intを実装してください。targetが存在しない場合は -1 を返してください。int、string、float64の 3 つの型について、正しく動作することを確認してください。 -
上級問題(難易度 ⭐⭐):汎用的な
Queue[T any](先入れ先出しキュー)を実装してください。要件:(1) enqueue、dequeue、peek、isEmpty メソッドを実装すること;(2) 任意の型に対応すること;(3) 頻繁なサイズ変更を避けるため、リングバッファを実装すること;(4)-raceを使用して並行処理の安全性を検証すること。 -
課題(難易度 ⭐⭐⭐):汎用的な 並行処理に安全なキャッシュ
Cache[K comparable, V any]を実装してください。要件:(1) Get/Set/Delete/Clear メソッド;(2) RWMutex による保護;(3) TTL による有効期限切れ処理;(4)OnEvictedコールバックのサポート(キーが削除されたり有効期限が切れたりした際に呼び出される);(5) ジェネリックを使用して、キーの型が比較可能であることを保証すること。



