ジェネリクス
ジェネリクス(型パラメータ)を使うと、型を抽象化した汎用的な関数や型を定義できます。Go 1.18で導入され、コードの再利用性が大幅に向上しました。
構文
// 型パラメータ付き関数を定義する
func 関数名[T 型制約](引数 T) T {
// 処理
}
// 型パラメータ付き構造体を定義する
type 型名[T 型制約] struct {
フィールド T
}
// 複数の型制約を定義する(インターフェースで表現する)
type 制約名 interface {
型1 | 型2 | 型3
}
主な型制約
| 制約 | 概要 |
|---|---|
| any | すべての型を受け入れます。『interface{}』と同等です。 |
| comparable | 『==』と『!=』で比較可能な型に限定します(スライスやマップは含まれません)。 |
| ~T | チルダ(~)は「Tを基底型とする全ての型」を意味します。独自型も対象になります(例:『~int』はintを基底型とする全ての型)。 |
| int | float64 | ユニオン型制約です。指定した型のいずれかを受け入れます。 |
| constraints.Ordered | 『golang.org/x/exp/constraints』パッケージで定義される順序比較が可能な型です。※外部パッケージのため『go get golang.org/x/exp/constraints』が必要です。 |
サンプルコード
sample_generics.go
package main
import "fmt"
// 数値型の制約を定義する
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
// ジェネリックな合計関数
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// ジェネリックなマップ変換関数
func Map[T, U any](slice []T, f func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
// ジェネリックなフィルタ関数
func Filter[T any](slice []T, pred func(T) bool) []T {
var result []T
for _, v := range slice {
if pred(v) {
result = append(result, v)
}
}
return result
}
// 型パラメータ付きのスタック構造体
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
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
// comparableを使ったジェネリック関数
func Contains[T comparable](slice []T, item T) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
func main() {
// Sum関数を異なる型で使う
ints := []int{1, 2, 3, 4, 5}
floats := []float64{1.1, 2.2, 3.3}
fmt.Println(Sum(ints)) // 『15』と出力されます。
fmt.Println(Sum(floats)) // 浮動小数点の加算結果が出力されます。
// Map関数で整数を文字列に変換する
nums := []int{1, 2, 3}
strs := Map(nums, func(n int) string {
return fmt.Sprintf("item%d", n)
})
fmt.Println(strs) // 『[item1 item2 item3]』と出力されます。
// Filter関数で偶数だけを取り出す
evens := Filter(ints, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // 『[2 4]』と出力されます。
// ジェネリックなスタックを使う
s := &Stack[string]{}
s.Push("Go")
s.Push("Generics")
v, ok := s.Pop()
fmt.Println(v, ok) // 『Generics true』と出力されます。
// Contains関数を文字列スライスに使う
langs := []string{"Go", "Python", "Rust"}
fmt.Println(Contains(langs, "Go")) // 『true』と出力されます。
fmt.Println(Contains(langs, "Java")) // 『false』と出力されます。
}
generics.go
go run generics.go 15 6.6 [item1 item2 item3] [2 4] Generics true true false
概要
ジェネリクスを使うと、型ごとに同じ処理を繰り返し書かずに済みます。例えば、整数のスライスと浮動小数点数のスライスを同じ関数で処理できます。型推論が働くため、ほとんどの場合は型パラメータを明示せずに呼び出せます。
型パラメータ付きの関数はメソッドとして定義できません。型パラメータを追加できるのは関数と型定義のみです。
『comparable』制約は『==』による比較が可能な型(int、string、構造体など)に限定します。スライスやマップは比較不可のため含まれません。ジェネリクスの濫用はコードを複雑にするため、明確な型安全性の向上が見込める場面に限定して使用することを推奨します。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。