【初心者向け】Goのジェネリクス入門
はじめに
現在僕はあるプロジェクトのバックエンド開発者として参画しています。
そのプロジェクトではGoをバックエンド言語として使用しているのですが、僕自身は過去にGoを使って開発したことがない状態で参画しているので、開発をしながら同時並行でスキル習得を進めるという体制をとっています
※常に未知との遭遇の毎日ですが、すごくチャレンジングでめちゃくちゃ楽しいです!
今週はGoのジェネリクスに触れる機会があったため、ジェネリクスについて自身で調べたこと、使いこなせるようになるために自身で解いた練習問題をこの記事にまとめたいと思います。
この記事がこれからGoを学習する人にとって少しでも役に立てば嬉しいです。
ジェネリクスとは何か?・・・その前に
Goの関数はその引数や戻り値が基本的に固定されています。
例えば具体例として以下の関数を見てみましょう。
func SumInts(m map[string]int) int {
var sum int
for _, v := range m {
sum += v
}
return sum
}
この関数は引数で渡したmap内のint型の数字を合計した結果を返す関数です。
これはこれで何の問題もないのですが、当然ですがこの関数は数値が { string型:int型} のmapのみを引数に取り、結果をint型で返すことしかできません。
とすると、 { string型:float型} のmapを引数に取りたいケースが出てきた時は、以下のようにfloat型専用の関数をもう一つ作成しなければなりません。
func SumFLoats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
ロジックはすべて一緒なのに、引数や戻り値が異なるためだけに新たに関数を作成しなければならないのは非常に冗長ですよね。
もしロジックに修正が発生した場合は、それぞれの関数のロジックについて修正しなければならず、保守面からも負担が大きくなる構造となってしまいます。
このようなケースに活用したいのがジェネリクスです。
ジェネリクスとは?
ジェネリクスとは一言で言うと、「型に依存しない汎用的なコードを安全に書くための仕組み」です。
「型に依存しない」とは先ほどの例のような関数について、int型やfloat型に左右されずコード作成ができる、と言うことです。
基本構文
func 関数名[型パラメータ](引数 型パラメータ) 戻り値 {
// 処理
}
具体的に先ほどの関数をジェネリクスを使って書いてみると以下のようになります。
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V{
var s V
for _, v := range m {
s += v
}
return s
}
ちょっとごちゃごちゃ書いていて何のことか分かりづらいかと思いますが、ポイントは以下です。
- 関数名と引数の間に「型パラメータ」と呼ばれる、型のヒント情報を追加する
- 使用する具体的な型(例:int, stringなど)は、関数を実際に呼び出す時に明示的に指定する(もしくはGoコンパイラが推論する)
具体的に呼び出す時は以下のように呼び出せます。
// int64型を合算する場合
SumIntsOrFloats[string, int64](ints)
// float64型を合算する場合
SumIntsOrFloats[string, float64](floats))
このようにジェネリクスを使用して関数を作成すれば、先ほどまで型に応じて(int64型とfloat64型)2つの関数として定義しなければならなかったSumも、一つにまとめて管理することができるようになります。
ではここで、ジェネリクスの肝である型パラメータの部分をもう少し詳しくみてみましょう
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V{
今回の型パラメータの内容をまとめると以下の通りになります
- 「K」と「V」という2つの値について定義されている
- 「K」は「comparable」の条件を満たす値である
- 「V」は「int64」または「float64」の型のみを受け付ける値である
- 関数に渡される引数は、上記の条件を満たしたmapである(map[K]V)
*キーが「K」、バリューが「V」 - 関数の戻り値は「V」に当てはまる型=int64かfloat64のみとなる
ここで「comparableとなんぞや?」という疑問を持った方がいるかもしれません。
comparableとは簡単に言うと『「==」や「!=」などで比較可能な値のみ受け付ける』というものになります。
他にも「ordered」といったものもありますが、今回はあまり深入りしないこととします。
より詳しく内容を知りたい方はこちらの公式ページを参照すると良いです。
以上がジェネリクスの基本情報です。
実際に手を動かして作ってみよう!
ここまでで基本的な理論は学べました。
でも理論だけ知っていても、使いこなせるようにはなりません。
自分で使いこなせるようにするためには、学んだ理論をアウトプットする必要があります。
そこで強制的にアウトプットするために以下の問題に挑戦してみましょう。
問題を4問準備しましたので、ぜひ挑戦してみて下さい
回答はまとめて最後の章に記載しておきます。
問題1:整数スライスを逆順にする関数をジェネリクスで書こう
要件は以下の通りです。
・任意の型のスライスを受け取り、それを逆順にして返す関数 ReverseSlice を作ってください。
・ジェネリクスを使って、[]int, []string, []float64 などに対応できるようにしてください。
具体的な実行例は以下の通りです。
reversed := ReverseSlice([]int{1, 2, 3}) // => [3, 2, 1]
reversedStr := ReverseSlice([]string{"a", "b", "c"}) // => ["c", "b", "a"]
問題2:スライスの中に特定の値が含まれているかチェックする関数を作ろう
要件は以下の通りです。
・ジェネリクスを使って、Contains[T comparable](list []T, target T) bool という関数を作成してください。
・「T」は比較可能な型(int, stringなど)である必要があります。
具体的な実行例は以下の通りです。
IsContains([]int{1, 2, 3}, 2) // => true
IsContains([]string{"go", "java"}, "python") // => false
問題3:Map関数をジェネリクスで実装しよう
要件は以下の通りです。
・任意の型のスライスに対して、任意の関数を適用して別のスライスを返す Map 関数を作成してください。
・以下のような関数定義にして下さい
func ConvertMap[T any, R any](input []T, fn func(T) R) []R
具体的な実行例は以下の通りです。
doubles := ConvertMap([]int{1, 2, 3}, func(x int) int {
return x * 2
}) // => [2, 4, 6]
lengths := ConvertMap([]string{"go", "lang"}, func(s string) int {
return len(s)
}) // => [2, 4]
問題4:Filter関数をジェネリクスで実装しよう
要件は以下の通りです。
・スライスから、条件を満たす要素だけを抽出する関数を作ってください
・以下のような関数定義にして下さい。
func ValueFilter[T any](input []T, fn func(T) bool) []T
具体的な実行例は以下の通りです。
isEven := func(n int) bool { return n%2 == 0 }
result := ValueFilter([]int{1, 2, 3, 4}, isEven) // => [2 4]
この4問についてのコードを作成できるようになれば、ジェネリクスの基本を概ね理解できていると言えます。
なので回答を見る前に、ぜひ自分自身の手でコードを作成してみて下さい。
作ったコードが間違っていても全然問題ありません。
むしろ「ああでもない、こうでもない」と試行錯誤する時間にこそ価値があります。
自分で考え、苦戦することでしか本当のスキルは身につきません。
AI駆動でのコード作成が全盛になっている現在からすると逆行しているように思えるかもしれませんが、むしろこの時代だからこそ、基礎をしっかりと身につけることが重要です。
その基礎があれば、AIが作成したコードが正しいのか、間違っているのか(要件に適しているのか、適していないのか)がしっかりと判断できます。
逆に基礎がついていないと、AIの作成したコードをそのまま信じるしかない状態となってしまいます。
でもそれだとあなたが開発者である理由はありませんよね?
あなたが「開発者」としての価値を出すためにも、自身の手でコードを作成して基礎をしっかりと理解する、という泥臭い経験はむしろ超貴重になってきます(だってみんなやらないから)。
なので自分自身の価値を上げるためにも、ぜひ自ら手を動かす泥臭い経験を大事にして下さい!
解答
ここからは上記の問題についての解答を記載していきます。
あえて解説は載せません。
なぜそうなるのか?についてChatGPTなどを活用しながらぜひ調べてみて下さい。
自分で調べるというその経験もあなたのスキル習得に非常に役に立ちますので、ぜひそちらも併せて実践してみて下さい。
なお、今回の解答よりもさらに良い解答がある場合は全然考えられますので、そのような解答を作成された方はぜひコメントで教えて下さい!
解答1
// 関数
func ReverseSlice[V any](s []V) []V {
l := len(s)
// 最初にmakeでスライスを初期化
reversed := make([]V, l)
for i, v := range s {
reversed[l-i-1] = v
}
return reversed
}
// 使用例
reversed := ReverseSlice[int64]([]int64{1,2,3,4,5})
fmt.Printf("Reversed Slice: %v\n", reversed)
// 結果
Reversed Slice: [5 4 3 2 1]
// 使用例
reversedString := ReverseSlice[string]([]string{"a", "b", "c", "d", "e"})
fmt.Printf("Reversed String: %v\n", reversedString)
// 結果
Reversed String: [e d c b a]
解答2
// 関数
func IsContains[K comparable](list []K, target K) bool {
for _, v := range list {
if v == target {
return true
}
}
return false
}
// 使用例
IsContains[int64]([]int64{1,2,3,4,5}, 3)
fmt.Printf("Contains 3: %v\n", IsContains([]int64{1,2,3,4,5}, 3))
// 結果
Contains 3: true
// 使用例
IsContains[string]([]string{"Go","TypeScript","Python"},"Java")
fmt.Printf("Contains Java: %v\n", IsContains([]string{"Go","TypeScript","Python"},"Java"))
// 結果
Contains Java: false
解答3
// 関数
func ConvertMap[T any, R any](input []T, fn func(T) R) []R {
output := make([]R, len(input))
for i, v := range input {
output[i] = fn(v)
}
return output
}
// 使用例
doubles := ConvertMap[int64, int64]([]int64{1,2,3,4,5}, func(x int64) int64 {
return x * 2
})
fmt.Printf("Doubled Values: %v\n", doubles)
// 結果
Doubled Values: [2 4 6 8 10]
// 使用例
checkResults := ConvertMap[string, string]([]string{"Go", "TypeScript", "Python"}, func(x string) string {
if x == "Go" {
return "Goが見つかりました"
} else {
return "Goではありませんでした"
}
})
fmt.Printf("Check Results: %v\n", checkResults)
// 結果
Check Results: [Goが見つかりました Goではありませんでした Goではありませんでした]
解答4
// 関数
func ValueFilter[T any](input []T, fn func(T) bool) []T{
output := make([]T, 0)
for _, v := range input {
if fn(v) {
output = append(output, v)
}
}
return output
}
//使用例
isEven := func(n int64) bool { return n%2 == 0 }
result := ValueFilter[int64]([]int64{1,2,3,4,5}, isEven)
fmt.Printf("Filtered Even Numbers: %v\n", result)
// 結果
Filtered Even Numbers: [2 4]
以上となります!
次週もアウトプットが出せるように引き続きGoのスキルアップを続けていきます!
Discussion