前回
初めに
前回は複数のモジュールを開発する際に使用する、マルチモジュール・ワークスペースの機能を紹介いたしました。
今回は関数や型を宣言して使用するためのジェネリクスについて学習していきます。
公式チュートリアルはこちら。
モジュールの作成
まずは作業場所にgenericsフォルダを作成し、コマンドプロンプトでgenericsフォルダに移動します。その後、go mod init example/genericsコマンドを実行してモジュールを作成しましょう。
ジェネリクスではない関数を追加
まずはmapの値を足し合わせて合計を返す関数を2つ追加します。genericsディレクトリの中にmain.goを作成しましょう。2つ宣言するのはint64の値を格納するmapと、floatの値を格納するmapの2種類のmapを扱うためです。
package main
import"fmt"
func main() {
// int64 用 map の初期化
ints := map[string]int64{
"first": 34,
"second": 12,
}
// float 用 map の初期化
floats := map[string]float64{
"first": 35.98,
"second": 26.99,
}
fmt.Printf("Non-Generic Sums: %v and %v\n",
SumInts(ints),
SumFloats(floats))
}
// mの値を加算していく
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
// mの値を加算していく
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}main.goファイルを保存してgo run .コマンドで実行すると以下のような計算結果が出力されます。
$ go run .
Non-Generic Sums: 46 and 62.97
複数の型を扱うジェネリクス関数を追加
先ほどは型が違うだけで同じ処理の関数を二種類用意しました。このような設計だと変更する際に片方だけ変更してしまい、バグにつながってしまうなどの問題が起こってしまいます。そこでジェネリクス関数を作成して一つの関数に置き換えていきます。
ジェネリクス関数の追加
先ほど作成した二種類の関数の下に以下のコードを追加しましょう。
// map の m の値をすべて合計した値を返す。IntとFloatの両方に対応している。
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}- 関数名の後ろに
()の間に[]で用意されたKとVの型パラメーターが用意され、型パラメーターを使用したmap[K]V型の引数mが登場しました。そして戻り値はVを指定しています。 - 型パラメーター
Kに制約であるcomparableを指定しています。comparableはGoで事前に宣言されているため、importする必要はありません。これは、値は比較演算子==や!=のオペランドとして使用される可能性がある任意の型を許可します。 - Goでは
mapのキーを比較可能な型であることを要求しますので、Kは比較可能であると宣言する必要があります。 - 型パラメーター
Vにはint64とfloat64の二つの型のどちらかであると指定します。 - 引数
mはmap[K]V型であることを指定しています。このKとVは先ほど指定した型で、Kは比較可能な型であるのでmap[K]Vが有効であることがわかります。Kが比較可能でない場合はコンパイルエラーとなります。
続いて追加した関数を使用するようにmain関数を変更していきます。main関数の最後に以下のコードを追加してください。
func main() {
// ...
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints),
SumIntsOrFloats[string, float64](floats))
}ジェネリクス関数のSumIntsOrFloatsを呼び出して、作成したmapをそれぞれ渡します。関数を呼び出す際に型引数になる型を明示的に渡しているため、[]で囲ってKにはstring、Vにはint64やfloat64を指定します。
実行確認
genericsディレクトリに移動してgo run .コマンドを実行し確認してみましょう。以下のような結果が出力されたら成功です。
$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
ジェネリクス関数呼び出し時の型引数を削除
先ほどはジェネリクス関数の呼び出し時に[]を使用して型引数の型を渡しました。今回は型引数は関数の引数に使用しているため、関数の引数の型からGoコンパイラが推測することが可能です。その場合は型引数を省略することが可能になります。
先ほど追加した、ジェネリクス関数の呼び出し処理の下にコードを追加してみましょう
func main() {
// ...
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
SumIntsOrFloats(ints),
SumIntsOrFloats(floats))
}コードを追加したら再度go run .を実行して確認してみましょう。以下のような結果が出力されたら、型引数の省略に成功しています。
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
型制約
先ほど定義した制約を再利用するために、独自のインターフェースに移動してみましょう。インターフェース化することで、複雑化の防止や、コードのメンテナンス性向上に役立てることができます。
import文とmain関数の間に以下のコードを追加してみましょう。
type Number interface {
int64 | float64
}これは型制約として使用するインターフェースの宣言になります。このインターフェースではint64とfloat64のユニオンを宣言することで、制約時にint64 | float64ではなくNumber型制約に置き換えることができます。
続いてmain.goの最後に以下の関数を追加しましょう。
// map mの値を合計する。map の値として整数と浮動小数点数の両方をサポートしている。
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}型パラメーターVの制約をNumber型制約に置き換えて簡潔に表すことができました。続いて以下のコードをmain関数の最後に追加します。
func main() {
// ...
fmt.Printf("Generic Sums with Constraint: %v and %v\n",
SumNumbers(ints),
SumNumbers(floats))
}コードを追加したら、go run .を実行し確認してみましょう。
$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97
まとめ
ジェネリクスは以下の構文で構成されます。
- 宣言時
func ジェネリクス関数名[型パラメーター](引数) 戻り値 {}- 型パラメータは
型パラメータ名 型名で宣言される。 - 型パラメータ名は大文字を使用する
- 型パラメータは
,区切りで複数指定可能。 - 型パラメータの型指定時に
|を使用することで、複数の型に対応することができる。
- 型パラメータは
- 呼び出し時
ジェネリクス関数[型引数](引数)- 型引数を使用した引数がある場合は型引数を省略することが可能。

