ジェネリクス(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
}