JavaScriptを有効にしてください

Go 1.18で実装されたジェネリクスを使ってみる

 ·   ·  ☕ 9 分で読めます  ·  🐈 もふもふ

Go 1.18で実装されたジェネリクスを使ってみる

Go 1.18で待望のジェネリクスが実装されましたので早速使ってみました。

ジェネリクスとは

ジェネリクスとは一つの関数で複数の型に対応できるようにGo 1.18で追加された新しい言語仕様です。

Go 1.17までは一つの関数に付き一つの型にしか対応できませんでしたが、

1
2
3
4
5
6
7
8
//int64型しか対応できない…
func SumInt(a, b int64) int64 {
	return a + b
}
//float64型しか対応できない…
func SumFloat(a, b float64) float64 {
	return a + b
}

Go 1.18で追加されたジェネリクスを使用すると下記のようにint64型またはfloat64型といった感じで複数の型が指定できるようになります。

1
2
3
4
//int64型またはfloat64型が指定できる!
func SumIntOrFloat[T int64 | float64](a, b T) T {
	return a + b
}

型がTという定義に置き換えられて、引数と戻り値の型がTになっていますね。

このように1.17までは複数の型に対応するため型毎に関数を書く必要がありましたが、1.18からジェネリクスを使用することによって、一つの関数で複数の型に対応できるようになります。
とても便利ですね😀

では早速使い方をみていきましょう。

ジェネリクスの使い方

まずは非ジェネリクス関数を定義して、それをジェネリクス関数に置き換えることで違いをみていきましょう。

非ジェネリクス関数を定義

int64型とfloat64型を変数として渡すとそれぞれ加算した結果を返す簡単な関数を定義します。

1
2
3
4
5
6
7
8
//int64型の足し算
func SumInt(a, b int64) int64 {
	return a + b
}
//float64型の足し算
func SumFloat(a, b float64) float64 {
	return a + b
}

こちらは見慣れた非ジェネリクスの関数です。
呼び出して使ってみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var i1, i2 int64
var f1, f2 float64

i1 = 3
i2 = 8
f1 = 3.5
f2 = 8.8
//非ジェネリクス関数
fmt.Println(SumInt(i1, i2))
fmt.Println(SumFloat(f1, f2))
1
2
11
12.3

それぞれのデータ型に対して、対応した関数を呼び出して処理しています。

では次はジェネリクス関数を定義して置き換えてみましょう。

ジェネリクス関数を定義

下記のように関数名の後にブラケットを記述しその中にデータ型を定義するとジェネリクス関数になります。

1
2
3
func SumIntOrFloat[T int64 | float64](a, b T) T {
	return a + b
}

このブラケット内に記述したデータ型のことを型引数と言います。

最初に型引数の名称を定義して(今回はT)関数内で使用するデータ型を「|」で繋いで記述していきます。
「|」はOR演算で使用していると思うので理解しやすいと思います。

では、ジェネリクス関数を呼び出して使ってみましょう。

1
2
3
//ジェネリクス関数
fmt.Println(SumIntOrFloat[int64](i1, i2))
fmt.Println(SumIntOrFloat[float64](f1, f2))
1
2
11
12.3

呼び出すときはブラケット内に使用するデータ型を記述します。

ブラケット内にデータ型を記述して呼び出していますが、今回の場合コンパイラが引数から型を推測することができるので、下記のように型引数を省略することができます。

1
2
3
//型を省略
fmt.Println(SumIntOrFloat(i1, i2))
fmt.Println(SumIntOrFloat(f1, f2))

i1やi2がint32型だった場合は推測ができないので、その場合は型引数を指定してあげましょう。

VSCodeなどのエディタを使用していると下記画像のように「これは省略して書けるよ!」といった補足が出るので省略できるケースに慣れていきましょう。
unnecessary type argumentsinfertypeargs(不要な型引数)
不要な型引数(unnecessary type arguments infertypeargs)と言われていますね😅

型制約を定義する

ところでジェネリクス関数で型引数を記述しましたが、データ型が多くなってくると毎回書くのはめんどくさいですよね🙄
そこでtype 〇〇 interfaceを使用して下記のように型制約を定義することができます。

1
2
3
type Number interface {
    int64 | float64
}

このNumberはint64とfloat64だけでしか使用できない型制約です。

この型制約を使用して先ほどのジェネリクス関数を書き換えると下記のようになります。

1
2
3
func SumNumber[T Number](a, b T) T {
	return a + b
}

int64とfloat64がNumberという名称に置き換わってとてもスッキリしました😊

このように型制約を自分で定義してもいいですが、constraintsという制約パッケージ が用意されていますのでこちらで使えるものがないか確認しておきましょう。

以上がジェネリクス関数の基本的な使い方になります。

次は型制約につける~トークンについてみていきましょう。

型制約の~(チルダ)トークン

ここまでの解説で型制約を定義して、関数の型引数に型を指定すれば、その型で関数が処理できるようになることが分かったと思います。

ところで下記のように自分で定義した型はどうなるのでしょうか。

1
2
type MyInt64 int64
type MyFloat64 float64

MyInt64として型を再定義していますが元にしている型がint64なので問題がないように見えます。
これをSumNumberで指定してみましょう。

1
2
3
4
5
6
7
8
9
var mi1, mi2 MyInt64
var mf1, mf2 MyFloat64
mi1 = 3
mi2 = 8
mf1 = 3.5
mf2 = 8.8
//↓はエラー(MyInt64 does not implement Number (possibly missing ~ for int64 in constraint Number))
//fmt.Println(SumNumber(mi1, mi2))
//fmt.Println(SumNumber(mf1, mf2))

これは残念ながらMyInt64 does not implement Numberというエラーになります。
これはNumberという型制約にMyInt64というデータ型が含まれていないためです。

このように型制約は~トークンを付けていない場合、その型と完全一致しなければ受け入れてくれません。
エラー内容をみると「int64の~が欠落している可能性があります」(possibly missing ~ for int64 in constraint Number)と教えてくれてますね。

基本型がint64のものを許可する場合は、下記のように冒頭に~トークンを付けてあげると許可されるようになります。

1
2
3
type TildeTokenNumber interface {
	~int64 | ~float64
}

TildeTokenNumberで定義した関数に渡してみるとエラーが出なくなりました。

1
2
3
4
5
6
fmt.Println(SumTildeTokenNumber(mi1, mi2))
fmt.Println(SumTildeTokenNumber(mf1, mf2))

func SumTildeTokenNumber[T TildeTokenNumber](a, b T) T {
	return a + b
}

特に基本型以外のものを許可しないルールがないのであれば、~トークンは付けておくようにしましょう。

次は少し複雑な例として引数にmapを使用したジェネリクス関数を見ていきましょう。

引数にmapを使用したジェネリクス関数の例

ここからは公式のチュートリアルで使用されているコードを元にして解説します。
チュートリアル:ジェネリクス入門

今回使用するコードはint64型またはfloat64型のmapを渡すとその合計を計算してくれる関数です。

1
2
3
4
5
6
7
8
//int64またはfloat64の合計を計算して返却
func SumIntsOrFloats[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

Vは先ほどまでに解説したNumberの型制約の名称です。
Kが今回の解説対象のcomparable型になります。

comparableについて

comparableは、Go 1.18で新たに追加された、==!=で比較可能なデータ型の集合を示すインターフェースになります。
こちらのインターフェースはGoに組み込まれているものです。

なぜmapのキーがcomparableになるのかというと、mapのキーに指定できるものは==!=で比較可能な下記の型に限定されているためです。

ブール型、数値型、文字列型、ポインタ型、チャネル型、インターフェイス型、およびこれらの型のみを含む構造体または配列です。
引用元:Go maps in action - Key types

比較可能でないとキーからデータの引き当てができませんからね🙄

ではcomparableについて理解したところで先ほどのジェネリクス関数を使ってみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ints := map[string]int64{
	"first":  3,
	"second": 8,
}
floats := map[string]float64{
	"first":  3.5,
	"second": 8.8,
}
fmt.Println(SumIntsOrFloats[string, int64](ints))
fmt.Println(SumIntsOrFloats[string, float64](floats))
1
2
11
12.3

一番目の型引数にcomparableなstring型を指定して、2番目に引数のデータ型を指定しています。

またこの場合も引数から型引数をコンパイラが推測可能なため、下記のように省略して書くことができます。

1
2
fmt.Println(SumIntsOrFloats(ints))
fmt.Println(SumIntsOrFloats(floats))

当然ですがmapのキーの型制約は必ずしもcomparableである必要はなく、別途型制約を定義して使うことも可能です。
その時の実装によって使い分けていきましょう。

まとめ

いかがでしたか。
ジェネリクスを使えば型毎の関数を定義することなく、一つの関数定義で複数の型に対応できるので今後かなりプログラムが書きやすくなると思います。
ただまだ1.18はリリースされたばかりですし、組み込みのパッケージもジェネリクスに対応していないため使用用途はかなり限定されるでしょう。

今後少しずつジェネリクスで実装されたコードが増えてくると思いますので、その時のために理解を深めておきましょう。

プログラム全体

今回のジェネリクスの解説で使用したすべてのコードをのせています。
コピーしてGo Playground に貼り付ければ実行できますので、実際の動作を確認してみてください。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
package main

import (
	"fmt"
)

type Number interface {
	int64 | float64
}

type TildeTokenNumber interface {
	~int64 | ~float64
}

type MyInt64 int64
type MyFloat64 float64

func main() {
	//-----------------------------------------------------------
	//ジェネリクスの使い方
	//-----------------------------------------------------------
	var i1, i2 int64
	var f1, f2 float64

	i1 = 3
	i2 = 8
	f1 = 3.5
	f2 = 8.8
	//非ジェネリクス関数
	fmt.Println("非ジェネリクス関数")
	fmt.Println(SumInt(i1, i2))
	fmt.Println(SumFloat(f1, f2))
	//ジェネリクス関数
	fmt.Println("ジェネリクス関数")
	fmt.Println(SumIntOrFloat[int64](i1, i2))
	fmt.Println(SumIntOrFloat[float64](f1, f2))
	//型を省略
	fmt.Println("型を省略")
	fmt.Println(SumIntOrFloat(i1, i2))
	fmt.Println(SumIntOrFloat(f1, f2))
	//型制約を使用する
	fmt.Println("型制約を使用する")
	fmt.Println(SumNumber(i1, i2))
	fmt.Println(SumNumber(f1, f2))
	//-----------------------------------------------------------
	//型制約の~(チルダ)トークン
	//-----------------------------------------------------------
	var mi1, mi2 MyInt64
	var mf1, mf2 MyFloat64
	mi1 = 3
	mi2 = 8
	mf1 = 3.5
	mf2 = 8.8
	//↓はエラー(MyInt64 does not implement Number (possibly missing ~ for int64 in constraint Number))
	//fmt.Println(SumNumber(mi1, mi2))
	//fmt.Println(SumNumber(mf1, mf2))
	fmt.Println("型制約の~(チルダ)トークン")
	fmt.Println(SumTildeTokenNumber(mi1, mi2))
	fmt.Println(SumTildeTokenNumber(mf1, mf2))
	//-----------------------------------------------------------
	//引数にmapを使用したジェネリクス関数の例
	//-----------------------------------------------------------
	ints := map[string]int64{
		"first":  3,
		"second": 8,
	}
	floats := map[string]float64{
		"first":  3.5,
		"second": 8.8,
	}
	fmt.Println("引数にmapを使用したジェネリクス関数の例")
	fmt.Println(SumIntsOrFloats[string, int64](ints))
	fmt.Println(SumIntsOrFloats[string, float64](floats))
	//省略バージョン
	fmt.Println("省略バージョン")
	fmt.Println(SumIntsOrFloats(ints))
	fmt.Println(SumIntsOrFloats(floats))
}

//int64型の足し算
func SumInt(a, b int64) int64 {
	return a + b
}

//float64型の足し算
func SumFloat(a, b float64) float64 {
	return a + b
}

//int64型またはfloat64型の足し算
func SumIntOrFloat[T int64 | float64](a, b T) T {
	return a + b
}

//Number型制約の足し算
func SumNumber[T Number](a, b T) T {
	return a + b
}

//チルダトークン付きのNumber型制約の足し算
func SumTildeTokenNumber[T TildeTokenNumber](a, b T) T {
	return a + b
}

//int64またはfloat64の合計を計算して返却
func SumIntsOrFloats[K comparable, V Number](m map[K]V) V {
	var s V
	for _, v := range m {
		s += v
	}
	return s
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
非ジェネリクス関数
11
12.3
ジェネリクス関数
11
12.3
型を省略
11
12.3
型制約を使用する
11
12.3
型制約の~(チルダ)トークン
11
12.3
引数にmapを使用したジェネリクス関数の例
11
12.3
省略バージョン
11
12.3

実行環境

1
2
$ go version
go version go1.18 linux/amd64

スポンサーリンク

共有

もふもふ
著者
もふもふ
プログラマ。汎用系→ゲームエンジニア→Webエンジニア