チュートリアル: ジェネリクス入門

このチュートリアルでは、Go のジェネリクスの基本について説明します。ジェネリクスを使用すると、呼び出し元のコードが提供する一連の型のいずれかで動作するように記述された関数や型を宣言して使用できます。

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

以下のセクションを進めていきます。

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

注:他のチュートリアルについては、チュートリアルをご覧ください。

注: 必要であれば、「Go dev branch」モードの Go playground を使用して、プログラムを編集・実行することもできます。

前提条件

  • Go 1.18 以降のインストール。 インストール手順については、Go のインストール を参照してください。
  • コードを編集するツール。 任意のテキストエディターで問題ありません。
  • コマンドターミナル. Go は Linux および Mac の任意のターミナル、および Windows の PowerShell または cmd でうまく動作します。

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

まず、作成するコード用のフォルダーを作成します。

  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 つの関数を追加します。

`int64` 値を格納するマップと `float64` 値を格納するマップという、2 種類の異なるマップを扱っているため、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))
    }
    

    このコードでは、以下のことを行います。

    • `float64` 値のマップと `int64` 値のマップをそれぞれ 2 つのエントリで初期化します。
    • 先に宣言した 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 つの型パラメータ (`K` と `V`、角括弧内) と、型パラメータを使用する 1 つの引数 `m` (型 `map[K]V`) を持つ `SumIntsOrFloats` 関数を宣言します。この関数は型 `V` の値を返します。
    • `K` 型パラメータには型制約 `comparable` を指定します。`comparable` 制約は、このようなケースのために特に意図されており、Go で事前宣言されています。これは、比較演算子 `==` および `!=` のオペランドとして値を使用できる任意の型を許可します。Go は、マップのキーが比較可能である必要があると要求します。したがって、`K` を `comparable` として宣言することは、`K` をマップ変数のキーとして使用できるようにするために必要です。また、呼び出し元のコードがマップキーに許可される型を使用することも保証します。
    • `V` 型パラメータには、`int64` と `float64` の 2 つの型の結合である制約を指定します。`|` を使用すると、2 つの型の結合を指定し、この制約がいずれかの型を許可することを意味します。いずれの型も、呼び出し元のコードで引数としてコンパイラによって許可されます。
    • `m` 引数は `map[K]V` 型であることを指定します。ここで `K` と `V` は型パラメータにすでに指定されている型です。`K` が比較可能な型であるため、`map[K]V` が有効なマップ型であることがわかります。`K` を `comparable` と宣言していなかった場合、コンパイラは `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` の既存のコードの下に、次のコードを貼り付けます。

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

    このコードでは、以下のことを行います。

    • 型引数を省略してジェネリック関数を呼び出します。

コードを実行する

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` インターフェース型を宣言します。

    • インターフェース内に `int64` と `float64` の結合を宣言します。

      基本的に、関数の宣言から新しい型制約に結合を移動しています。そうすることで、型パラメータを `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
}