LoginSignup
1
1

Go言語のジェネリクスについて

Last updated at Posted at 2024-03-01

はじめに

Go言語のジェネリクスは、Go 1.18から導入され、異なるデータ型に対応可能な汎用的な関数や型を作成することができます。他の言語におけるジェネリクスと同様、Goのジェネリクスもまた、型パラメータを用いて抽象化されたコードを書くことができます。たとえば、C#やJavaでは長年ジェネリクスが利用されており、コレクションライブラリなどでその力を発揮していますし、TypeScriptのような言語では、ジェネリクスを使ってより柔軟かつ型安全なコードを書くことができます。
少しサンプルコードを参考として使い方を紹介しようかと思います。

考え方

ジェネリクスがよくわからん。という人のために少し説明すると、「型に柔軟性を持たせる仕組み」です。プログラミングするとき、色んなデータ型がありますよね。整数や文字列、あるいはもっと複雑なカスタム型まで。でも、多くの場合、異なる型であっても「同じような処理」をしたい時があります。そこで登場するのがジェネリクスです。簡単に言うと、ジェネリクスを使うと「この関数は、どんな型にも対応するよ」と宣言できるんです。例えば、配列の要素を逆順にする関数を作るとき、整数の配列にも、文字列の配列にも、ほかの何かの配列にも使える一つの関数を作ることができるわけです。

この「どんな型にも対応する」というのがポイントで、ジェネリクスを使うと、特定の型を指定する代わりに「型パラメータ」というものを使って、関数や型を定義します。実際に関数を呼び出すときには、「この関数、今回は整数の配列で使うよ」とか「文字列の配列で使うね」と指定するわけです。

スライスの最初の要素を返す関数

任意の型 T のスライスを受け取り、スライスの最初の要素を返します。返す値は2つあり、一つ目がスライスの最初の要素、二つ目がその要素が存在するかどうかを表すブール値です。

func First[T any](s []T) (T, bool) {
	if len(s) > 0 {
		return s[0], true
	}
	var zero T
	return zero, false
}

この、[T any] という部分がそれを示しています。T は型パラメータで、any は「どんな型でも良い」という意味です。つまり、この関数は int のスライス、string のスライス、あるいはユーザー定義型のスライスなど、どんな型のスライスにも対応しています。
var zero T の部分は、T 型の変数 zero を宣言し、その型のゼロ値(デフォルト値)で初期化しています。Go言語では、変数を宣言した時に初期値を明示的に指定しない場合、その型のゼロ値が自動的に割り当てられます。

各型のゼロ値は以下の通りです:

  • 数値型(intfloat64 など)のゼロ値は 0
  • ブーリアン型(bool)のゼロ値は false
  • 文字列型(string)のゼロ値は空文字列("")。
  • ポインタ、スライス、マップ、チャネル、関数、インターフェースなどの参照型のゼロ値は nil
    したがって、var zero T というコード行では、T というジェネリック型パラメータに対して、その型に応じたゼロ値を持つ変数 zero を宣言しています。例えば、Tint 型であれば、zero の値は 0 になりますし、Tstring 型であれば、zero の値は空文字列("")になります。

この zero 変数は、関数 First でスライス s が空(len(s) == 0)の場合に、ゼロ値を返すために使用されます。このようにして、First 関数はスライスが空の場合にも、適切な型のゼロ値を返すことができるようになっています。これにより、関数がジェネリックであるにもかかわらず、型安全性を保ちながら、あらゆる型のスライスに対応することが可能になります。

任意の型のスライスを受け取り、その要素を逆順にする関数

任意の型 T のスライスを受け取り、その要素の順序を逆にして返します。

func Reverse[T any](s []T) []T {
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
		s[i], s[j] = s[j], s[i]
	}
	return s
}

この関数は、スライス s の最初の要素と最後の要素を交換し、次に2番目の要素と最後から2番目の要素を交換する、という処理を繰り返します。この交換は、スライスの中央に達するまで続けられます。for ループの条件 i < j が、この「中央に達するまで」という処理を制御しています。

T any という部分がジェネリクスの型パラメータ定義で、これにより Reverse 関数は int 型のスライス、string 型のスライス、さらにはユーザー定義型のスライスなど、任意の型のスライスに対応することができます。これは、ジェネリクスを使うことで、一つの関数で多様な型を扱えるようになるというジェネリクスの強力な利点を示しています。

マップエントリのフィルタリング: キーが偶数の要素だけを抽出するジェネリック関数

この関数は、マップのキーと値の両方に対して条件を適用し、その条件を満たすエントリだけを含む新しいマップを返します。

package main

import "fmt"

// FilterMap関数は、マップmと、キーと値のペアに対する条件を表す関数predicateを引数に取ります。
// この関数は、predicateがtrueを返すすべてのエントリで構成される新しいマップを返します。
func FilterMap[K comparable, V any](m map[K]V, predicate func(K, V) bool) map[K]V {
    result := make(map[K]V)
    for k, v := range m {
        if predicate(k, v) {
            result[k] = v
        }
    }
    return result
}

func main() {
    // 整数をキーとし、文字列を値とするマップの例
    m := map[int]string{
        1: "a",
        2: "b",
        3: "c",
        4: "d",
    }

    // マップからキーが偶数のエントリだけを抽出する
    filtered := FilterMap(m, func(k int, v string) bool {
        return k%2 == 0
    })

    fmt.Println(filtered) // 出力: map[2:b 4:d]
}

このコード例では、FilterMap はジェネリクスを利用して、任意の型 K のキーと Vの値を持つマップに対して動作するように設計されています。K には comparable 制約があり、これはキーとして使用できる型が比較可能である必要があることを意味します(マップのキーとして使用するためには、この制約が必要です)。

predicate 関数は、マップの各エントリ(キーと値のペア)に対して適用され、この関数が true を返すエントリのみが結果のマップに含まれます。これにより、非常に柔軟なフィルタリング処理が可能になり、様々な条件でマップのエントリを選択的に抽出できます。

終わり

というわけで、Go言語のジェネリクスについて、いくつかのサンプルコードを元に最初の基本的な例から、もう少し複雑なフィルタリングの例まで、ジェネリクスがどんなに便利かが分かったかと思います。
ジェネリクスは、一見するとちょっと難しそうに見えるかもしれませんが、慣れてくると本当に便利なツールです。型安全を保ちつつ、コードの重複を減らし、様々なデータ型で同じロジックを再利用できるので、よりクリーンで読みやすいコードを書くことができます。
これを機会に色々と調べるのも楽しいかもしれません。

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1