ふわっとテック日記

テック系のことをメインに書いていきます。go言語/typescript/javascriptが好きです。たまに趣味(筋トレ)の話や日常系の記事も書きたいな〜と思っています。

Go言語でのジェネリクス(1.18から)

Go言語ではバージョン1.18から待望のジェネリクスが実装されたのですが、

普段の業務でジェネリクスを使うことがあまりなく仕様をよく分かっていなかったので

備忘録的に残しておこうと思います。

ジェネリクスとは

関数の引数となるデータ型が複数考えられる時、考えられる全てのデータ型を受け入れるように関数を実装できる仕組みのこと。

関数呼び出し時に引数と一緒に引数の型も指定する。

関数定義時点では引数の型が分からないといった場合に、柔軟性のある関数を実装することができます。

Goでの実装例

例えば下のSumInt関数とSumStringは、それぞれ引数の2つのint, stringを足し合わせた結果を返す関数です。

当然、SumIntの引数にstring型は指定できないし、SumStringの引数にint型は指定できません。

func SumInt(num1 int, num2 int) int {
    return num1 + num2
}
    
func SumString(str1 string, str2 string) string {
    return str1 + str2
}

ここで、引数の型は関数実行時にしかわからないけど、とにかく引数の2つの値を足し合わせた結果を返す関数を定義したい、とします。

ここでジェネリクスを使った関数を定義します。

func SumIntOrString[T int | string](arg1, arg2 T) T {
    return arg1 + arg2
}

ここで定義したTは型パラメータと呼ばれるもので、実行時に取りうるTの型を表します。

この関数では2つの引数と返り値の型がTとなります。

int | stringはユニオンと呼ばれるもので、intかstringのどちらかになれるよ、という意味です。そのものずばり、型制約と呼ばれたりします。

この関数を実行してみましょう。

func main() {
    res := SumIntOrString[int](4, 5)
    fmt.Println(res)  // 9

    res := SumIntOrString[string]("qwe", "rty")
    fmt.Println(res) // qwerty
}

こんな感じで、関数名の直後に型引数[]で型パラメータの型を決めてあげます。

また、引数が型パラメータで定義されている場合は関数実行時の型引数は省略できます。コンパイラが型パラメータを参考に取りうる型を推測してくれるからです。

func main() {
    res := SumIntOrString(4, 5) // 型引数なしでもOK
    fmt.Println(res)  // 9

    res := SumIntOrString("qwe", "rty") // 型引数なしでもOK
    fmt.Println(res) // qwerty
}

引数が型パラメータで定義されていない場合(引数自体がない場合など)は、型引数は省略できないのでご注意を。


ジェネリクス関数を複数定義するときに毎回同じ型制約を指定する時などは、いちいち同じ型制約を全ての関数に書いていると煩雑です。

そんな時はインターフェースで型制約を定義して使いまわしてしまいましょう。

type DefaulOption interface {
    int | string
}

func SumIntOrString[T DefaulOption](arg1, arg2 T) T {
    return arg1 + arg2
}


comparableやanyなど、組み込み済みの型制約インターフェースも使用できます。

// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}

// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface{ comparable }

Underlying Type

型パラメータの型にチルダが付いているパターン。このチルダはUnderlying Typeと呼ばれます。

func SumIntOrString[T ~int | ~string](arg1, arg2 T) T {
    return arg1 + arg2
}

これはUnderlying Typeがint/stringの型ならOKという意味です。

例えばこんな感じです。

func SumIntOrString[T int | string](arg1, arg2 T) T {
    return arg1 + arg2
}

func SumUnderlyingIntOrString[T ~int | ~string](arg1, arg2 T) T {
    return arg1 + arg2
}

type NewInt int // NewIntはUnderlying Typeがintの型

func main() {
    var num1 NewInt = 3
    var num2 NewInt = 5

    fmt.Println(SumIntOrString(num1, num2)) // これはエラー

    fmt.Println(SumUnderlyingIntOrString(num1, num2)) // これはセーフ
}

Underlying Typeの分かりやすい説明記事を発見したのでご参考までに。