チュートリアル: ジェネリクスの始め方

このチュートリアルでは、Goにおけるジェネリクスの基本を紹介します。ジェネリクスを使うと、呼び出し元のコードが提供する型の集合のいずれでも動作するように書かれた関数や型を宣言および使用できます。

このチュートリアルでは、2つの単純な非ジェネリック関数を宣言し、同じロジックを1つのジェネリック関数にまとめます。

次のセクションを進めていきます

  1. コード用のフォルダを作成します。
  2. 非ジェネリック関数を追加します。
  3. 複数の型を処理するためのジェネリック関数を追加します。
  4. ジェネリック関数を呼び出すときに型引数を削除します。
  5. 型制約を宣言します。

注意: 他のチュートリアルについては、チュートリアルを参照してください。

注意: 必要であれば、“Go dev ブランチ” モードのGo playgroundを使用して、プログラムを編集および実行することもできます。

前提条件

コード用のフォルダを作成します

最初に、記述するコード用のフォルダを作成します。

  1. コマンドプロンプトを開き、ホームディレクトリに移動します。

    LinuxまたはMacの場合

    $ cd
    

    Windowsの場合

    C:\> cd %HOMEPATH%
    

    チュートリアルの残りの部分では、$がプロンプトとして表示されます。使用するコマンドはWindowsでも動作します。

  2. コマンドプロンプトから、genericsという名前のコード用のディレクトリを作成します。

    $ mkdir generics
    $ cd generics
    
  3. コードを保持するためのモジュールを作成します。

    go mod initコマンドを実行し、新しいコードのモジュールパスを指定します。

    $ go mod init example/generics
    go: creating new go.mod: module example/generics
    

    注意: 本番環境のコードでは、独自のニーズに合わせてより具体的なモジュールパスを指定します。詳細については、必ず依存関係の管理を参照してください。

次に、マップを操作するための簡単なコードを追加します。

非ジェネリック関数を追加します

このステップでは、マップの値を合計して合計を返す2つの関数を追加します。

2つの異なる型のマップ(int64値を格納するマップとfloat64値を格納するマップ)を操作するため、1つではなく2つの関数を宣言しています。

コードを記述します

  1. テキストエディタを使用して、genericsディレクトリにmain.goというファイルを作成します。このファイルにGoコードを記述します。

  2. main.goの先頭に、次のパッケージ宣言を貼り付けます。

    package main
    

    (ライブラリとは対照的に)スタンドアロンプログラムは、常にmainパッケージに含まれます。

  3. パッケージ宣言の下に、次の2つの関数宣言を貼り付けます。

    // SumInts adds together the values of m.
    func SumInts(m map[string]int64) int64 {
        var s int64
        for _, v := range m {
            s += v
        }
        return s
    }
    
    // SumFloats adds together the values of m.
    func SumFloats(m map[string]float64) float64 {
        var s float64
        for _, v := range m {
            s += v
        }
        return s
    }
    

    このコードでは、

    • マップの値を合計して合計を返す2つの関数を宣言します。
      • SumFloatsは、stringからfloat64値へのマップを受け取ります。
      • SumIntsは、stringからint64値へのマップを受け取ります。
  4. main.goの先頭のパッケージ宣言の下に、次のmain関数を貼り付けて、2つのマップを初期化し、前のステップで宣言した関数を呼び出すときにそれらを引数として使用します。

    func main() {
        // Initialize a map for the integer values
        ints := map[string]int64{
            "first":  34,
            "second": 12,
        }
    
        // Initialize a map for the float values
        floats := map[string]float64{
            "first":  35.98,
            "second": 26.99,
        }
    
        fmt.Printf("Non-Generic Sums: %v and %v\n",
            SumInts(ints),
            SumFloats(floats))
    }
    

    このコードでは、

    • それぞれ2つのエントリを持つfloat64値のマップとint64値のマップを初期化します。
    • 前に宣言した2つの関数を呼び出して、各マップの値の合計を見つけます。
    • 結果を出力します。
  5. main.goの先頭付近のパッケージ宣言のすぐ下に、記述したコードをサポートするために必要なパッケージをインポートします。

    コードの最初の行は次のようになります

    package main
    
    import "fmt"
    
  6. main.goを保存します。

コードを実行します

main.goを含むディレクトリのコマンドラインから、コードを実行します。

$ go run .
Non-Generic Sums: 46 and 62.97

ジェネリクスを使用すると、ここで2つではなく1つの関数を記述できます。次に、整数または浮動小数点値を含むマップ用の単一のジェネリック関数を追加します。

複数の型を処理するためのジェネリック関数を追加します

このセクションでは、整数値または浮動小数点値のいずれかを含むマップを受け取ることができる単一のジェネリック関数を追加し、事実上、記述したばかりの2つの関数を1つの関数に置き換えます。

どちらの型の値もサポートするには、その単一の関数がサポートする型を宣言する方法が必要です。一方、呼び出し元のコードには、整数マップと浮動小数点マップのどちらで呼び出すかを指定する方法が必要です。

これをサポートするために、通常の関数パラメータに加えて型パラメータを宣言する関数を記述します。これらの型パラメータは関数をジェネリックにし、異なる型の引数で動作できるようにします。関数は、型引数と通常の関数引数を使用して呼び出します。

各型パラメータには、型パラメータのメタ型として機能する型制約があります。各型制約は、呼び出し元のコードがそれぞれの型パラメータに使用できる許可された型引数を指定します。

型パラメータの制約は通常、型のセットを表しますが、コンパイル時には型パラメータは単一の型(呼び出し元のコードによって型引数として提供される型)を表します。型引数の型が型パラメータの制約で許可されていない場合、コードはコンパイルされません。

型パラメータは、ジェネリックコードが実行するすべての操作をサポートする必要があることに注意してください。たとえば、関数のコードが、制約に数値型が含まれている型パラメータに対してstring操作(インデックス作成など)を実行しようとすると、コードはコンパイルされません。

これから記述するコードでは、整数型または浮動小数点型を許可する制約を使用します。

コードを記述します

  1. 以前に追加した2つの関数の下に、次のジェネリック関数を貼り付けます。

    // SumIntsOrFloats sums the values of map m. It supports both int64 and float64
    // as types for map values.
    func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
        var s V
        for _, v := range m {
            s += v
        }
        return s
    }
    

    このコードでは、

    • 2つの型パラメータ(角かっこ内)、KV、および型パラメータm(型map[K]V)を使用する1つの引数を持つSumIntsOrFloats関数を宣言します。関数はV型の値を返します。
    • K型パラメータに型制約comparableを指定します。特にこのようなケースを対象としたcomparable制約は、Goであらかじめ宣言されています。これにより、値が比較演算子==および!=のオペランドとして使用できる任意の型を使用できます。Goでは、マップキーが比較可能である必要があります。したがって、Kcomparableとして宣言することは、Kをマップ変数内のキーとして使用できるようにするために必要です。また、呼び出し元のコードがマップキーに許可された型を使用するようにします。
    • V型パラメータに、2つの型(int64float64)の和集合である制約を指定します。|を使用すると、2つの型の和集合が指定され、この制約がいずれかの型を許可することを意味します。いずれかの型は、コンパイラによって呼び出し元のコードでの引数として許可されます。
    • m引数がmap[K]V型であることを指定します。ここで、KVは型パラメータにすでに指定されている型です。Kが比較可能な型であるため、map[K]Vが有効なマップ型であることがわかります。Kを比較可能と宣言しなかった場合、コンパイラはmap[K]Vへの参照を拒否します。
  2. main.goで、すでにあるコードの下に、次のコードを貼り付けます。

    fmt.Printf("Generic Sums: %v and %v\n",
        SumIntsOrFloats[string, int64](ints),
        SumIntsOrFloats[string, float64](floats))
    

    このコードでは、

    • 作成した各マップを渡して、宣言したばかりのジェネリック関数を呼び出します。

    • 呼び出している関数の型パラメータを置き換える必要がある型を明確にするために、型引数(角かっこ内の型名)を指定します。

      次のセクションで説明するように、多くの場合、関数呼び出しで型引数を省略できます。Goは多くの場合、コードからそれらを推測できます。

    • 関数によって返される合計を出力します。

コードを実行します

main.goを含むディレクトリのコマンドラインから、コードを実行します。

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97

コードを実行するために、コンパイラは各呼び出しで型パラメータをその呼び出しで指定された具体的な型に置き換えました。

記述したジェネリック関数を呼び出すときに、関数の型パラメータの代わりにどの型を使用するかをコンパイラに伝える型引数を指定しました。次のセクションで説明するように、多くの場合、コンパイラが推測できるため、これらの型引数を省略できます。

ジェネリック関数を呼び出すときに型引数を削除します

このセクションでは、ジェネリック関数の呼び出しの修正バージョンを追加し、呼び出し側のコードを簡略化するための小さな変更を加えます。ここでは、不要な型引数を削除します。

Goコンパイラが使用したい型を推測できる場合、呼び出し元のコードで型引数を省略できます。コンパイラは、関数引数の型から型引数を推測します。

これは常に可能ではないことに注意してください。たとえば、引数のないジェネリック関数を呼び出す必要がある場合は、関数呼び出しに型引数を含める必要があります。

コードを記述します

コードを実行します

main.goを含むディレクトリのコマンドラインから、コードを実行します。

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97

次に、整数と浮動小数点の和集合を、他のコードなどから再利用できる型制約にキャプチャすることで、関数をさらに簡略化します。

型制約を宣言します

この最後のセクションでは、前に定義した制約を独自のインターフェイスに移動して、複数の場所で再利用できるようにします。このように制約を宣言すると、制約が複雑な場合など、コードを合理化するのに役立ちます。

型制約をインターフェイスとして宣言します。制約は、インターフェイスを実装する任意の型を許可します。たとえば、3つのメソッドを持つ型制約インターフェイスを宣言し、それをジェネリック関数の型パラメータで使用すると、関数を呼び出すために使用される型引数は、それらのすべてのメソッドを持っている必要があります。

制約インターフェイスは、このセクションで説明するように、特定の型を参照することもできます。

コードを記述します

  1. mainのすぐ上で、importステートメントの直後に、次のコードを貼り付けて型制約を宣言します。

    type Number interface {
        int64 | float64
    }
    

    このコードでは、

    • 型制約として使用するNumberインターフェイス型を宣言します。

    • インターフェイス内でint64float64の和集合を宣言します。

      基本的に、関数の宣言から和集合を新しい型制約に移動しています。これにより、型パラメータをint64またはfloat64のいずれかに制約したい場合に、int64 | float64と記述する代わりに、このNumber型制約を使用できます。

  2. すでにある関数の下に、次のジェネリックSumNumbers関数を貼り付けます。

    // SumNumbers sums the values of map m. It supports both integers
    // and floats as map values.
    func SumNumbers[K comparable, V Number](m map[K]V) V {
        var s V
        for _, v := range m {
            s += v
        }
        return s
    }
    

    このコードでは、

    • 以前に宣言したジェネリック関数と同じロジックを持つジェネリック関数を宣言しますが、型制約として共用体ではなく新しいインターフェース型を使用します。以前と同様に、引数と戻り値の型には型パラメータを使用します。
  3. main.goで、すでにあるコードの下に、次のコードを貼り付けます。

    fmt.Printf("Generic Sums with Constraint: %v and %v\n",
        SumNumbers(ints),
        SumNumbers(floats))
    

    このコードでは、

    • 各マップに対してSumNumbersを呼び出し、それぞれの値の合計を出力します。

      前のセクションと同様に、ジェネリック関数を呼び出す際には型引数(角括弧内の型名)を省略します。Goコンパイラは、他の引数から型引数を推論できます。

コードを実行します

main.goを含むディレクトリのコマンドラインから、コードを実行します。

$ 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

結論

よくできました!これでGoにおけるジェネリクスを初めて体験しました。

次に推奨されるトピック

完成したコード

このプログラムはGo playgroundで実行できます。playgroundでは、単にRunボタンをクリックしてください。

package main

import "fmt"

type Number interface {
    int64 | float64
}

func main() {
    // Initialize a map for the integer values
    ints := map[string]int64{
        "first": 34,
        "second": 12,
    }

    // Initialize a map for the float values
    floats := map[string]float64{
        "first": 35.98,
        "second": 26.99,
    }

    fmt.Printf("Non-Generic Sums: %v and %v\n",
        SumInts(ints),
        SumFloats(floats))

    fmt.Printf("Generic Sums: %v and %v\n",
        SumIntsOrFloats[string, int64](ints),
        SumIntsOrFloats[string, float64](floats))

    fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
        SumIntsOrFloats(ints),
        SumIntsOrFloats(floats))

    fmt.Printf("Generic Sums with Constraint: %v and %v\n",
        SumNumbers(ints),
        SumNumbers(floats))
}

// SumInts adds together the values of m.
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

// SumIntsOrFloats sums the values of map m. It supports both floats and integers
// as map values.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

// SumNumbers sums the values of map m. Its supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}