Go言語のジェネリクス(Generics)の基本的な使い方|Queue, Setを実装してみた
![](https://tech.aru-zakki.com/wp-content/uploads/2024/04/go-generics.001.jpeg)
Go言語のジェネリクスの使い方について調べてみました。Go言語へのジェネリクスの実装は、比較的新しいものです。これを使うと関数の使い回しが少しだけ楽になります。
ジェネリクスとは
簡単に言うと、ジェネリクス(Generics)は、型をパラメータ化する機能のことです。この機能がある言語では、特定のデータ型に依存しないコードを書くことができます。
例えば、a+bを行う関数addIntを作成したい場合、a, bが整数の場合は、以下のような関数を作成します。
func addInt(a, b int) int {
return a+b
}
この関数は、入力が整数のため浮動小数点の計算を行う場合は、別の関数を用意する必要があります。例えば、以下のようになります。
func addFloat64(a, b float64) float64 {
return a+b
}
ジェネリクスがある場合は、この2つの関数をまとめて1つaddという関数で定義することができます。
ジェネリクスがあれば、特定の型に依存しないコードを書くことができるため、同じコードを再利用しやすくなります。
このジェネリクスは、プログラミング言語では一般的な機能でしたが、Go言語ではサポートしていませんでした。
ジェネリクスは、プログラミング言語において一般的な機能であり、コードの柔軟性や安全性を向上させるために広く利用されています。
Go言語でもバージョン1.18からジェネリクスは導入されました。ここでは、Go言語でジェネリクスを使う方法を解説します。
![](https://tech.aru-zakki.com/wp-content/uploads/2023/06/tabbycat.png)
Go言語でジェネリクスが導入されていなかった主な理由は「シンプルさと明快さの追求」です。Go言語の設計思想の1つは、シンプルさを重視することです。ジェネリクスは言語の複雑性を増し、コードの読みやすさを低下させる可能性があります。そのため、ジェネリクスを導入する場合には、この原則とのバランスを考慮する必要がありました。
ジェネリクスの使い方
基本的な使い方
基本的な使い方(1)
簡単なジェネリスクの使い方として、整数型と浮動小数点、文字列をサポートしたAdd関数を定義してみます(文字列の場合は、2つの文字列を連結する操作とする)。
Add
関数の定義は以下のようになります。
[T int | float64 | string]
の部分が関数が受け入れる型です。関数宣言のこの部分は型パラメータの定義で、この例ではint
, float64
, string
の3つを受け付けることが宣言されています。
T
を定義してたので、関数の引数や戻り値、内部ではT
が入力された型となります。
この関数では、入力に応じたa+b
が行われ戻り値として返されます。
func Add[T int | float64 | string](a, b T) T {
return a + b
}
もう1つ、引数として様々な型を受け付けるPrint
関数を定義します。この関数は、内部でfmt.Println
を呼び出すものです。
any
を用いることで様々な型を受け取ることが可能になります(内部的にはinterface{}
と同じ)。
func Print[T any](n T) {
fmt.Println(n)
}
この2つの関数を利用する場合は以下のようになります(コメント部は実行結果)
このように、引数として渡された型から自動的に、適切な呼び出しがおこなれます。
なお最後の行のPrint[int]
のように、型を指定して呼び出すことも可能です。
func main() {
Print(Add(1, 2))
// 3
Print(Add(1.2, 2))
// 3.2
Print(Add("a", "b"))
// ab
Print[int](10)
// 10
}
基本的な使い方(2)
type
を使って型に名前をつけておくことも可能です。
下記の例では、num
という型をint
またはfloat64
として定義し、Sub[T num]
と言う形で定義した型を利用して関数を宣言しています。
type num interface {
int | float64
}
func Sub[T num](a, b T) T {
return a - b
}
このように、type
を使うことで、コードをシンプルにすることが可能です。
キュー(Queue)を作ってみる
ジェネリクスを使って、いろいろな型を格納可能なキューを実装してみます。
Vector
という型を[]T
として定義しています。それぞれの関数は以下の処理を行います。
Queue
関数:キューを初期化しPush
関数:キューに値を入力Pop
関数:キューから値を取り出し
各関数の記述は、ジェネリクスを使っていることを除けば、Go言語でよく見かける記述なので説明は省略します。
type Vector[T any] []T
func Queue[T any]() *Vector[T] {
ret := make(Vector[T], 0)
return &ret
}
func (v *Vector[T]) Push(x T) {
*v = append(*v, x)
}
func (v *Vector[T]) Pop() T {
ret := (*v)[0]
*v = (*v)[1:]
return ret
}
これを使った例が以下になります。int
、float64
はもとより、定義した構造体Pair
もキューで取り扱うことができます。
type Pair struct {
a, b int
}
func main() {
// int
{
q := Queue[int]()
for i := 0; i < 10; i++ {
q.Push(i)
}
fmt.Println(q)
for len(*q) != 0 {
v := q.Pop()
fmt.Println(v, q)
}
}
// float64
{
q := Queue[float64]()
for i := 0; i < 10; i++ {
q.Push(float64(i) / 10)
}
fmt.Println(q)
for len(*q) != 0 {
v := q.Pop()
fmt.Println(v, q)
}
}
// Pair
{
q := Queue[Pair]()
for i := 0; i < 10; i++ {
q.Push(Pair{i, i * 10})
}
fmt.Println(q)
for len(*q) != 0 {
v := q.Pop()
fmt.Println(v, q)
}
}
}
Setを実装してみる
Set
をジェネリクスを使って実装してみます。
ここでは、any
ではなく、comparable
を使いました。comparable
を指定すると、型として指定できるのは、==
による値の比較ができる型限定になります。
それぞれの関数は以下の処理を行います。
Set
関数:Setを新規に作成するAdd
関数:Setに値を追加するIncludes
関数:値がSetに含まれているかどうかを返すRemove
関数:値をSetから削除する
内部では、map[T]bool
と、マップ形式で管理しています。
type _Set[T comparable] map[T]bool
func Set[T comparable](xs ...T) _Set[T] {
s := make(_Set[T])
for _, xs := range xs {
s.Add(xs)
}
return s
}
func (s _Set[T]) Add(x T) {
s[x] = true
}
func (s _Set[T]) Includes(x T) bool {
return s[x]
}
func (s _Set[T]) Remove(x T) {
delete(s, x)
}
以下が利用例です。
func main() {
s := Set(1, 2, 3, 3, 4)
fmt.Println(s)
s.Add(5)
fmt.Println(s.Includes(3))
s.Remove(3)
fmt.Println(s.Includes(3))
}
上記の例では、Setを整数(int
)で利用してますが、ジェネリクスを使っているのでfloat64
やstring
でも利用することが可能です。
このように、1つの関数を様々な型で使えるとことがジェネリクスの利点です
まとめ
ジェネリクスの使い方について解説しました。個人的にはC++でもテンプレートをあまり使わない派なので、ジェネリクスを利用することは少ないと思います。
ただ、ライブラリがジェネリクス対応に切り替わって行けば、かなり便利になるのではないかと感じています。