前提条件

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

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

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

  • 1. コマンドプロンプトを開き、ホームディレクトリに移動します。
    LinuxまたはMacの場合:
    1. $ cd
    Windowsの場合:
    1. C:\> cd %HOMEPATH%
    チュートリアルの残りの部分では、プロンプトとして$が表示されます。使用するコマンドはWindowsでも動作します。
  • 2. コマンドプロンプトから、コード用のディレクトリをgenericsという名前で作成します。
    1. $ mkdir generics
    2. $ cd generics
  • 3. コードを保持するためのモジュールを作成します。
    go mod initコマンドを実行し、新しいコードのモジュールパスを指定します。
    1. $ go mod init example/generics
    2. go: creating new go.mod: module example/generics
    注意: 本番コードの場合、より具体的なモジュールパスを指定します。詳細については、依存関係の管理を参照してください。

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

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

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

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

コードを書く

  • 1. テキストエディタを使用して、genericsディレクトリにmain.goという名前のファイルを作成します。このファイルにGoコードを書きます。
  • 2. main.goのファイルの先頭に、次のパッケージ宣言を貼り付けます。
    1. package main
    スタンドアロンプログラム(ライブラリとは対照的に)は常にパッケージmainにあります。
  • 3. パッケージ宣言の下に、次の2つの関数宣言を貼り付けます。

    1. // SumInts adds together the values of m.
    2. func SumInts(m map[string]int64) int64 {
    3. var s int64
    4. for _, v := range m {
    5. s += v
    6. }
    7. return s
    8. }
    9. // SumFloats adds together the values of m.
    10. func SumFloats(m map[string]float64) float64 {
    11. var s float64
    12. for _, v := range m {
    13. s += v
    14. }
    15. return s
    16. }

    このコードでは、

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

    1. func main() {
    2. // Initialize a map for the integer values
    3. ints := map[string]int64{
    4. "first": 34,
    5. "second": 12,
    6. }
    7. // Initialize a map for the float values
    8. floats := map[string]float64{
    9. "first": 35.98,
    10. "second": 26.99,
    11. }
    12. fmt.Printf("Non-Generic Sums: %v and %v\n",
    13. SumInts(ints),
    14. SumFloats(floats))
    15. }

    このコードでは、

    • float64の値のマップとint64の値のマップを初期化し、それぞれ2つのエントリを持ちます。
    • 以前に宣言した2つの関数を呼び出して、各マップの値の合計を求めます。
    • 結果を印刷します。
  • 5. main.goの先頭近くで、パッケージ宣言のすぐ下に、先ほど書いたコードをサポートするために必要なパッケージをインポートします。
    最初のコード行は次のようになります:

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

コードを実行する

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

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

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

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

このセクションでは、整数または浮動小数点値を含むマップを受け取る単一のジェネリック関数を追加します。これにより、先ほど書いた2つの関数を1つの関数に置き換えます。

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

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

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

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

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

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

コードを書く

  • 1. 以前に追加した2つの関数の下に、次のジェネリック関数を貼り付けます。
    1. // SumIntsOrFloats sums the values of map m. It supports both int64 and float64
    2. // as types for map values.
    3. func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    4. var s V
    5. for _, v := range m {
    6. s += v
    7. }
    8. return s
    9. }
    このコードでは、
    • 2つの型パラメータ(角括弧内)KVを持つSumIntsOrFloats関数を宣言し、型map[K]Vmという型パラメータを使用する1つの引数を持ちます。この関数は型Vの値を返します。
    • K型パラメータにcomparable型制約を指定します。このようなケースに特に意図されたcomparable制約は、Goで事前に宣言されています。これは、比較演算子==および!=のオペランドとして使用できる値を持つ任意の型を許可します。Goはマップのキーが比較可能であることを要求します。したがって、Kcomparableとして宣言することは、マップ変数のキーとしてKを使用できるようにするために必要です。また、呼び出しコードがマップキーに許可された型を使用することを保証します。
    • V型パラメータに、int64float64の2つの型の和集合である制約を指定します。|を使用すると、2つの型の和集合が指定され、この制約はどちらの型も許可します。どちらの型も、呼び出しコードの引数としてコンパイラによって許可されます。
    • m引数がmap[K]V型であることを指定します。ここで、KVは型パラメータに対してすでに指定された型です。map[K]Vが有効なマップ型であることは、Kが比較可能な型であるため、わかります。Kを比較可能として宣言しなかった場合、コンパイラはmap[K]Vへの参照を拒否します。
  • 2. main.goで、すでにあるコードの下に次のコードを貼り付けます。
    1. fmt.Printf("Generic Sums: %v and %v\n",
    2. SumIntsOrFloats[string, int64](ints),
    3. SumIntsOrFloats[string, float64](floats))
    このコードでは、
    • 先ほど宣言したジェネリック関数を呼び出し、作成した各マップを渡します。
    • 型引数(角括弧内の型名)を指定して、呼び出している関数の型パラメータを置き換えるべき型を明確にします。
      次のセクションで見るように、関数呼び出しで型引数を省略することがよくあります。Goはしばしばコードからそれらを推測できます。
    • 関数が返す合計を印刷します。

コードを実行する

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

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

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

あなたが書いたジェネリック関数を呼び出す際に、型引数を指定してコンパイラに関数の型パラメータの代わりに使用する型を伝えました。次のセクションで見るように、多くの場合、これらの型引数を省略できます。コンパイラはそれらを推測できます。

ジェネリック関数を呼び出す際に型引数を削除する

このセクションでは、呼び出しコードを簡素化するために、ジェネリック関数呼び出しの修正バージョンを追加します。型引数を削除しますが、これはこの場合には必要ありません。

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

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

コードを書く

  • main.goで、すでにあるコードの下に次のコードを貼り付けます。
    1. fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
    2. SumIntsOrFloats(ints),
    3. SumIntsOrFloats(floats))
    このコードでは、
    • 型引数を省略してジェネリック関数を呼び出します。

コードを実行する

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

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

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

型制約を宣言する

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

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

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

コードを書く

  • 1. mainのすぐ上に、インポート文の直後に、型制約を宣言するために次のコードを貼り付けます。
    1. type Number interface {
    2. int64 | float64
    3. }
    このコードでは、
    • 型制約として使用するNumberインターフェース型を宣言します。
    • インターフェース内にint64float64の和集合を宣言します。
      本質的に、関数宣言から和集合を新しい型制約に移動しています。これにより、int64またはfloat64のいずれかに型パラメータを制約したい場合、このNumber型制約を使用できます。int64 | float64を書く必要はありません。
  • 2. すでにある関数の下に、次のジェネリックSumNumbers関数を貼り付けます。
    1. // SumNumbers sums the values of map m. It supports both integers
    2. // and floats as map values.
    3. func SumNumbers[K comparable, V Number](m map[K]V) V {
    4. var s V
    5. for _, v := range m {
    6. s += v
    7. }
    8. return s
    9. }
    このコードでは、
    • 以前に宣言したジェネリック関数と同じロジックを持つジェネリック関数を宣言しますが、型制約として和集合の代わりに新しいインターフェース型を使用します。以前と同様に、引数と戻り値の型に型パラメータを使用します。
  • 3. main.goで、すでにあるコードの下に次のコードを貼り付けます。
    1. fmt.Printf("Generic Sums with Constraint: %v and %v\n",
    2. SumNumbers(ints),
    3. SumNumbers(floats))
    このコードでは、
    • 各マップでSumNumbersを呼び出し、各マップの値から合計を印刷します。
      前のセクションと同様に、ジェネリック関数への呼び出しで型引数(角括弧内の型名)を省略します。Goコンパイラは他の引数から型引数を推測できます。

コードを実行する

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

  1. $ go run .
  2. Non-Generic Sums: 46 and 62.97
  3. Generic Sums: 46 and 62.97
  4. Generic Sums, type parameters inferred: 46 and 62.97
  5. Generic Sums with Constraint: 46 and 62.97

結論

素晴らしい!Goのジェネリクスに初めて触れました。

次にお勧めのトピック:

  • Goツアーは、Goの基本を段階的に紹介する素晴らしいリソースです。
  • Effective GoGoコードの書き方に記載されている有用なGoのベストプラクティスを見つけることができます。

完成したコード

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

  1. package main
  2. import "fmt"
  3. type Number interface {
  4. int64 | float64
  5. }
  6. func main() {
  7. // Initialize a map for the integer values
  8. ints := map[string]int64{
  9. "first": 34,
  10. "second": 12,
  11. }
  12. // Initialize a map for the float values
  13. floats := map[string]float64{
  14. "first": 35.98,
  15. "second": 26.99,
  16. }
  17. fmt.Printf("Non-Generic Sums: %v and %v\n",
  18. SumInts(ints),
  19. SumFloats(floats))
  20. fmt.Printf("Generic Sums: %v and %v\n",
  21. SumIntsOrFloats[string, int64](ints),
  22. SumIntsOrFloats[string, float64](floats))
  23. fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
  24. SumIntsOrFloats(ints),
  25. SumIntsOrFloats(floats))
  26. fmt.Printf("Generic Sums with Constraint: %v and %v\n",
  27. SumNumbers(ints),
  28. SumNumbers(floats))
  29. }
  30. // SumInts adds together the values of m.
  31. func SumInts(m map[string]int64) int64 {
  32. var s int64
  33. for _, v := range m {
  34. s += v
  35. }
  36. return s
  37. }
  38. // SumFloats adds together the values of m.
  39. func SumFloats(m map[string]float64) float64 {
  40. var s float64
  41. for _, v := range m {
  42. s += v
  43. }
  44. return s
  45. }
  46. // SumIntsOrFloats sums the values of map m. It supports both floats and integers
  47. // as map values.
  48. func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
  49. var s V
  50. for _, v := range m {
  51. s += v
  52. }
  53. return s
  54. }
  55. // SumNumbers sums the values of map m. Its supports both integers
  56. // and floats as map values.
  57. func SumNumbers[K comparable, V Number](m map[K]V) V {
  58. var s V
  59. for _, v := range m {
  60. s += v
  61. }
  62. return s
  63. }