Go言語 ジェネリクス

Goは静的型付けプログラミング言語です。つまり、変数と関数はコンパイル時に特定の型が定義されている必要があります。

Go 1.18で導入されたジェネリクスを使うと、型ごとに別々の関数を書くのではなく、複数のデータ型を扱える1つの関数を書くことができます。これにより、コードがより簡潔になり、保守が容易になります。

Go言語 ジェネリクスとは?

ジェネリクスはプログラミング言語の機能で、さまざまなデータ型で動作する関数やデータ構造を書くことができます。ジェネリクスを使用すると、特定のデータ型に依存しないコードを書くことができます。つまり、整数、浮動小数点数、文字列、その他のデータ型を扱う関数を1つ書くことができます。

Goでは、ジェネリクスは主に型パラメータと型引数で表現されます。型パラメータは型のプレースホルダで、型引数は関数やデータ構造のインスタンス化に使用される特定の型です。

配列の要素の合計を計算する関数を考えてみましょう。以下のように、整数と浮動小数に対して別々の関数を書くことができます。

func SumInts(nums []int) int {
    var sum int
    for _, v := range nums {
        sum += v
    }
    return sum
}

func SumFloats(nums []float64) float64 {
    var sum float64
    for _, v := range nums {
        sum += v
    }
    return sum
}

見ての通り、これらの関数は入力パラメータの型と戻り値の型を除けば、ほとんど同じです。他のデータ型もサポートする場合、それぞれ別の関数を書く必要があります。

Go言語 ジェネリクスの使い方

ジェネリクスを使えば、どんなデータ型にも対応できる関数を1つ書くことができます。以下は、Goでジェネリクスを使ってsum関数を書く方法です。

package main

import (
    "fmt"
)

// 関数に対して型パラメータを新たに定義することで型を一般化できます
// このTはint型かfloat64型を受け付ける型パラメータになっています
// 関数内部ではint型かfloat64型が前提とした処理が行われます
func Sum[T int | float64](nums []T) T {
    var sum T
    for _, v := range nums {
        sum += v
    }
    return sum
}

func main() {
    ints := []int{1, 2, 3, 4, 5}
    floats := []float64{1.0, 2.0, 3.0, 4.0, 5.0}

    // ジェネリクス関数を実行する際には関数に対して型引数を渡します
    // int型で処理をさせたい場合はintを渡し、float64型で書ををさせたい場合はfloat64を渡します
    fmt.Println("Sum by ints result -> ", Sum[int](ints))
    fmt.Println("Sum by floats result -> ", Sum[float64](floats))
}

Sum関数の直後に付与されている[T]の部分を型パラメータと呼びます。ここでは、型パラメータTはint型かfloat64型のどちらかを受け付けるパラメータとして定義されています。

main関数では、intsとfloatの2つのスライスが作成されています。[int]や[float64]の部分を型引数と呼びます。ここでは、Sum関数をint型で処理させたい場合は型引数にintを渡し、Sum関数をfloat64型で処理させたい場合は型引数にfloat64を渡しています。プログラムが実行されると、intsスライスとfloatsスライスの合計が別々に出力されます。

Note: 型パラメータで指定された型が前提でコンパイルが行われるため、 型パラメータで指定した型にそぐわないような使い方はできません。例えば、この例ではstring(t)のキャストは使うことができません。なぜなら変数tはfloat64型の可能性もあるためです。

Go言語 ジェネリクスとany

先ほどは、型パラメータにint|float64と書くことでint型かfloat64型を許容するジェネリクスを構築しました。

しかし、anyキーワードを用いることで、もっと一般化されたジェネリクスを構築することもできます。anyというのはinterface{}のエイリアス(別名)になっていて、メソッドが定義されていないインターフェース型と同等の意味を持ちます。

例えば、配列の中身を順番に表示するジェネリクス関数を考えてみましょう。

package main

import (
    "fmt"
)

// 型パラメータがanyとなるジェネリクス
// anyはinterface{}と同義なので型引数としてどの型でも代入できます
func Display[T any](nums []T) {
    for _, v := range nums {
        fmt.Println(v)
    }
}

func main() {
    ints := []int{0, 1, 2}
    floats := []float64{10.0, 11.0, 12.0}
    strs := []string{"a", "b", "c"}

    // それぞれの型引数を代入することで関数を実行します
    Display[int](ints)
    Display[float64](floats)
    Display[string](strs)
}

anyというキーワードと共に型パラメータを定義したジェネリクスを作成しました。

anyはメソッドが定義されていないインターフェース型と同等の意味合いを持ちます。つまり、上記のジェネリクス関数は型引数として全ての型を許容するような関数となっています。次に、その型パラメータを用いて関数の引数である配列の型定義を行なっています。従って、任意の型の配列を受け取り、その中身を順次出力するといった処理が行われます。

先ほどのジェネリクスの例では、int型かfloat64型しか型として許容していませんでしたが、 今回はstring型も型引数として代入し実行できます。

Note: anyキーワードを用いて、先ほどの配列の合計値を計算することはできません。forループの中で行われる足し算はインターフェース型では行えないため、コンパイルエラーとなってしまいます。

それでは、インタフェースを活用して、配列の合計値を計算するジェネリクス関数を実装してみましょう。

package main

import (
    "fmt"
)

// Numericというインターフェースを用意します
type Numeric interface {
    int | float64
}

// TにNumberTypeを指定することで前の例と全く同じ処理内容となります
func Sum[T Numeric](nums []T) T {
    var sum T
    for _, v := range nums {
        sum += v
    }
    return sum
}

func main() {
    ints := []int{1, 2, 3, 4, 5}
    floats := []float64{1.0, 2.0, 3.0, 4.0, 5.0}

    // 型引数は型推論により省略可能
    fmt.Println("Sum by ints result -> ", Sum(ints))
    fmt.Println("Sum by floats result -> ", Sum(floats))
}

今度はanyではなく、Numericというインターフェースを用意し、それを型パラメータとして指定しています。Numericの中では先ほどと同様にint型かfloat64型を許容するような定義になっています。

また、今回は型引数を省略しています。型引数の部分は、Goのランタイムが関数に渡したパラメータの型から型引数を推論して自動的に渡してくれるため、型推論が行われるような場面では、上記のコードのように型引数は省略可能となっています。

この記事を書いた人

著者の画像

Jeffry Alvarado

Ex-Facebook Engineer 大学ではコンピュータサイエンスを専攻し、在学中に複数のインターンシップを経験。コンピュータサイエンスが学習できるプラットフォームRecursionを創業し、CTOとしてカリキュラム作成、ソフトウェア開発を担当。


ツイート