ジェネリクス(generics)

ジェネリクス(generics)は、型パラメータを使って複数の型に対して動作する汎用的なコードを書く仕組みです。 同じロジックを型ごとに重複して書く必要がなくなります。

Go 1.18以降: ジェネリクスが導入されました。

ジェネリクスが解決する問題

たとえば、スライスの要素を合計する関数を考えます。 ジェネリクスがない場合、型ごとに同じロジックを繰り返す必要があります。

func SumInt(s []int) int {
    var total int
    for _, v := range s {
        total += v
    }
    return total
}

func SumFloat64(s []float64) float64 {
    var total float64
    for _, v := range s {
        total += v
    }
    return total
}

ジェネリクスを使うと、1つの関数で複数の型を扱えます。

func Sum[T int | float64](s []T) T {
    var total T
    for _, v := range s {
        total += v
    }
    return total
}

func main() {
    fmt.Println(Sum([]int{1, 2, 3}))         // 6
    fmt.Println(Sum([]float64{1.1, 2.2, 3.3})) // 6.6000000000000005
}

型パラメータの書き方

型パラメータは関数名や型名の後ろの[]の中に宣言します。 型パラメータ名 型制約の形式で記述します。

// 形式: func 関数名[型パラメータ名 型制約](引数) 戻り値 { ... }
func 関数名[T any](v T) T {
    ...
}

型パラメータ名には慣例としてT(type)・K(key)・V(value)などの大文字1文字がよく使われます。

ジェネリクス関数

型パラメータを持つ関数をジェネリクス関数と呼びます。 呼び出す際に型引数を渡しますが、コンパイラが型を推論できる場合は省略できます。

func Map[T, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

func main() {
    nums := []int{1, 2, 3}

    // 型引数を明示する場合
    doubled := Map[int, int](nums, func(n int) int { return n * 2 })
    fmt.Println(doubled) // [2 4 6]

    // 型推論で省略できる場合
    strs := Map(nums, func(n int) string { return fmt.Sprintf("%d", n) })
    fmt.Println(strs) // [1 2 3]
}

型制約

型パラメータには型制約(type constraint)を指定します。 型制約は「どんな型を渡せるか」を定め、インタフェースとして記述します。

any

anyは任意の型を受け付ける制約です。interface{}のエイリアスです。 渡された値を別の関数に渡したり、そのまま返したりする用途に向いています。

func First[T any](s []T) (T, bool) {
    if len(s) == 0 {
        var zero T
        return zero, false
    }
    return s[0], true
}

func main() {
    v, ok := First([]int{10, 20, 30})
    fmt.Println(v, ok) // 10 true

    v2, ok := First([]string{})
    fmt.Println(v2, ok) // "" false
}

comparable

comparable==!=で比較できる型のみを受け付ける制約です。 マップのキーになれる型と同じです。

func Contains[T comparable](s []T, v T) bool {
    for _, e := range s {
        if e == v {
            return true
        }
    }
    return false
}

func main() {
    fmt.Println(Contains([]int{1, 2, 3}, 2))       // true
    fmt.Println(Contains([]string{"a", "b"}, "c")) // false
}

ユニオン型制約

|で複数の型を列挙した制約を作れます。 列挙した型に共通する演算(>+など)を型パラメータに対して使えます。

type Number interface {
    int | int8 | int16 | int32 | int64 |
        float32 | float64
}

func Min[T Number](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Min(3, 5))     // 3
    fmt.Println(Min(1.5, 0.8)) // 0.8
}

チルダ(~)による基底型の指定

制約に~Tと書くと、Tを基底型とするすべての型を許可します。 type MyInt intのような独自定義の型にも対応できます。

type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type UserID int
type PostID int

func Equal[T Integer](a, b T) bool {
    return a == b
}

func main() {
    fmt.Println(Equal(UserID(1), UserID(1))) // true
    fmt.Println(Equal(PostID(1), PostID(2))) // false
}

メソッドを含む制約

制約のインタフェースにはメソッドを含めることもできます。 型の列挙とメソッドを組み合わせることもできます。

type Stringer interface {
    String() string
}

func JoinStrings[T Stringer](s []T, sep string) string {
    if len(s) == 0 {
        return ""
    }
    result := s[0].String()
    for _, v := range s[1:] {
        result += sep + v.String()
    }
    return result
}

ジェネリクス型

型パラメータを持つ型をジェネリクス型と呼びます。 構造体などの型定義に型パラメータを付けられます。

type Stack[T any] struct {
    items []T
}

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

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

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

func main() {
    s := Stack[int]{}
    s.Push(1)
    s.Push(2)
    s.Push(3)
    fmt.Println(s.Len()) // 3

    v, ok := s.Pop()
    fmt.Println(v, ok) // 3 true
    v, ok = s.Pop()
    fmt.Println(v, ok) // 2 true
}

複数の型パラメータ

型パラメータは複数宣言できます。 カンマ区切りで並べます。

type Pair[K, V any] struct {
    Key   K
    Value V
}

func NewPair[K, V any](key K, value V) Pair[K, V] {
    return Pair[K, V]{Key: key, Value: value}
}

func main() {
    p := NewPair("age", 30)
    fmt.Println(p.Key, p.Value) // age 30
}

ゼロ値の取得

ジェネリクス関数の中で型パラメータのゼロ値が必要な場合は、var zero Tと宣言します。

func Zero[T any]() T {
    var zero T
    return zero
}

func main() {
    fmt.Println(Zero[int]())     // 0
    fmt.Println(Zero[string]())  // ""
    fmt.Println(Zero[bool]())    // false
}

制限事項

メソッドに独自の型パラメータを追加できない

型に定義するメソッドは、型が持つ型パラメータを使えますが、独自の型パラメータを追加できません。 型パラメータが必要な操作は通常の関数として定義します。

type Container[T any] struct {
    items []T
}

// OK: 型の型パラメータTを使う
func (c *Container[T]) Add(v T) {
    c.items = append(c.items, v)
}

// コンパイルエラー: メソッドに独自の型パラメータは追加できない
// func (c *Container[T]) Map[U any](f func(T) U) []U { ... }

// OK: 通常の関数として定義する
func MapContainer[T, U any](c Container[T], f func(T) U) []U {
    result := make([]U, len(c.items))
    for i, v := range c.items {
        result[i] = f(v)
    }
    return result
}

ユニオン型制約は変数の型に使えない

ユニオン型(int | float64など)を含むインタフェースは、型制約としてのみ使えます。

type Number interface {
    int | float64
}

// コンパイルエラー: ユニオンを含むインタフェースは変数の型に使えない
// var n Number = 1

// OK: 型制約としての使用
func Double[T Number](v T) T {
    return v * 2
}

参考リンク