Go言語 ジェネリクス
Goは静的型付けプログラミング言語です。つまり、変数と関数はコンパイル時に特定の型が定義されている必要があります。
Go 1.18で導入されたジェネリクスを使うと、型ごとに別々の関数を書くのではなく、複数のデータ型を扱える1つの関数を書くことができます。これにより、コードがより簡潔になり、保守が容易になります。
Go言語 ジェネリクスとは?
ジェネリクスはプログラミング言語の機能で、さまざまなデータ型で動作する関数やデータ構造を書くことができます。ジェネリクスを使用すると、特定のデータ型に依存しないコードを書くことができます。つまり、整数、浮動小数点数、文字列、その他のデータ型を扱う関数を1つ書くことができます。
Goでは、ジェネリクスは主に型パラメータと型引数で表現されます。型パラメータは型のプレースホルダで、型引数は関数やデータ構造のインスタンス化に使用される特定の型です。
スライスの要素の合計を計算する関数を考えてみましょう。以下のように、整数と浮動小数に対して別々の関数を書くことができます。
func SumInts(nums []int) int {
var sum int
for _, v := range nums {
sum += v
}
return sum
}
func SumFloats(nums []float64) float64 {
var sum float64
for _, v := range nums {
sum += v
}
return sum
}
見ての通り、これらの関数は入力パラメータの型と戻り値の型を除けば、ほとんど同じです。他のデータ型もサポートする場合、それぞれ別の関数を書く必要があります。
Go言語 ジェネリクスの使い方
ジェネリクスを使えば、複数のデータ型に対応できる関数を1つ書くことができます。以下は、Goでジェネリクスを使ってsum関数を書く方法です。
package main
import (
"fmt"
)
// 関数に対して型パラメータを新たに定義することで型を一般化できます
// このTはint型かfloat64型を受け付ける型パラメータになっています
// 関数内部ではint型かfloat64型が前提とした処理が行われます
func Sum[T int | float64](nums []T) T {
var sum T
for _, v := range nums {
sum += v
}
return sum
}
func main() {
ints := []int{1, 2, 3, 4, 5}
floats := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
// ジェネリクス関数を実行する際には関数に対して型引数を渡します
// int型で処理をさせたい場合はintを渡し、float64型で処理をさせたい場合はfloat64を渡します
fmt.Println("Sum by ints result -> ", Sum[int](ints))
fmt.Println("Sum by floats result -> ", Sum[float64](floats))
}
Sum関数の直後に付与されている[T int | float64]の部分を型パラメータリストと呼びます。Tが型パラメータで、int | float64はTが受け付ける型を決める型制約です。ここでは、型パラメータTはint型かfloat64型のどちらかを受け付けるパラメータとして定義されています。
main関数では、intsとfloatsの2つのスライスが作成されています。[int]や[float64]の部分を型引数と呼びます。ここでは、Sum関数をint型で処理させたい場合は型引数にintを渡し、Sum関数をfloat64型で処理させたい場合は型引数にfloat64を渡しています。プログラムが実行されると、intsスライスとfloatsスライスの合計が別々に出力されます。
Note: 型パラメータで指定された型が前提でコンパイルが行われるため、 型パラメータで指定した型にそぐわないような使い方はできません。例えば、この例ではstring(v)の変換は使うことができません。なぜなら変数vはfloat64型の可能性もあるためです。
Go言語 ジェネリクスとany
先ほどは、型パラメータにint|float64と書くことでint型かfloat64型を許容するジェネリクスを構築しました。
しかし、anyを用いることで、もっと一般化されたジェネリクスを構築することもできます。anyというのはinterface{}の別名、つまりエイリアスになっていて、メソッドが定義されていないインターフェース型と同等の意味を持ちます。
例えば、スライスの中身を順番に表示するジェネリクス関数を考えてみましょう。
package main
import (
"fmt"
)
// 型パラメータがanyとなるジェネリクス
// anyはinterface{}と同義なので型引数としてどの型でも代入できます
func Display[T any](nums []T) {
for _, v := range nums {
fmt.Println(v)
}
}
func main() {
ints := []int{0, 1, 2}
floats := []float64{10.0, 11.0, 12.0}
strs := []string{"a", "b", "c"}
// それぞれの型引数を代入することで関数を実行します
Display[int](ints)
Display[float64](floats)
Display[string](strs)
}
anyと共に型パラメータを定義したジェネリクスを作成しました。
anyはメソッドが定義されていないインターフェース型と同等の意味合いを持ちます。つまり、上記のジェネリクス関数は型引数として全ての型を許容するような関数となっています。次に、その型パラメータを用いて関数の引数であるスライスの型定義を行なっています。従って、任意の型のスライスを受け取り、その中身を順次出力するといった処理が行われます。
先ほどのジェネリクスの例では、int型かfloat64型しか型として許容していませんでしたが、 今回はstring型も型引数として代入し実行できます。
Note: anyを用いて、先ほどのスライスの合計値を計算することはできません。anyはどんな型も受け付けるため、+演算子を使えない型も含まれます。そのため、forループの中で行われる足し算は型パラメータTに対して行えず、コンパイルエラーとなってしまいます。
それでは、インタフェースを活用して、スライスの合計値を計算するジェネリクス関数を実装してみましょう。
package main
import (
"fmt"
)
// Numericというインターフェースを用意します
type Numeric interface {
int | float64
}
// TにNumericを指定することで前の例と全く同じ処理内容となります
func Sum[T Numeric](nums []T) T {
var sum T
for _, v := range nums {
sum += v
}
return sum
}
func main() {
ints := []int{1, 2, 3, 4, 5}
floats := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
// 型引数は型推論により省略可能
fmt.Println("Sum by ints result -> ", Sum(ints))
fmt.Println("Sum by floats result -> ", Sum(floats))
}
今度はanyではなく、Numericというインターフェースを用意し、それを型パラメータとして指定しています。Numericの中では先ほどと同様にint型かfloat64型を許容するような定義になっています。
また、今回は型引数を省略しています。型引数の部分は、Goのコンパイラが関数に渡したパラメータの型から型引数を推論して自動的に渡してくれるため、型推論が行われるような場面では、上記のコードのように型引数は省略可能となっています。