【Go言語入門⑦】ジェネリックを使い始める

Go言語
目次

過去の記事

下記の内容を実施している前提となりますのでご了承ください

ジェネリクスの概要

ジェネリクスなしの場合

func Add(a int, b int) int {
    return a+b
}

ジェネリクスありの場合

type Number interface {
	int64 | float64
}

func Add[T Number] (a T, b T) T{
    return a+b
}

ジェネリクスはTypeScriptの場合<>でしたが、Goの場合は[]で利用すると覚えれば問題ないです。

今回の内容

このチュートリアルでは、Go のジェネリックの基本を紹介します。ジェネリックを使用すると、呼び出しコードによって提供される一連の型のいずれかと連携するように記述された関数または型を宣言して使用できます。

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

次のセクションに進みます。

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

前提条件

  • Go 1.18 以降のインストール。インストール手順については、 Go のインストールを参照してください。
  • コードを編集するためのツール。お使いのテキスト エディタはどれでも問題なく動作します。
  • コマンド端末です。Go は、Linux と Mac の任意のターミナル、および Windows の PowerShell または cmd で適切に動作します。

モジュールの作成

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

注:運用コードの場合は、独自のニーズにより具体的なモジュール パスを指定します。詳細については、 依存関係の管理を参照してください。

パッケージを作成

モジュール内にパッケージを作成していきましょう

package main

import "fmt"

func main() {
	// 整数値用のマップを初期化する
	ints := map[string]int64{
		"first":  34,
		"second": 12,
	}

	// フロート値用のマップを初期化する
	floats := map[string]float64{
		"first":  35.98,
		"second": 26.99,
	}

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

// SumIntsはm個の値を足し合わせる。
func SumInts(m map[string]int64) int64 {
	var s int64
	for _, v := range m {
		s += v
	}
	return s
}

// SumFloatsはm個の値を足し合わせる。
func SumFloats(m map[string]float64) float64 {
	var s float64
	for _, v := range m {
		s += v
	}
	return s
}

こちらを作成したら、実行して動作確認します

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

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

// SumIntsOrFloatsは、マップmの値を合計します。
// マップの値の型として使用します
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
	var s V
	for _, v := range m {
		s += v
	}
	return s
}

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

  • SumIntsOrFloats関数を宣言し、2つの型パラメータ(角括弧の中)KとV、および型パラメータを使用する1つの引数、map[K]Vの型を持つ。この関数は型Vの値を返す.
  • K型パラメータに、型制約comparableを指定します。このような場合を特に想定して、Goではcomparable制約が事前に宣言されています。この制約は、値が比較演算子==や!=のオペランドとして使用できる任意の型を許可します。したがって、K をマップ変数のキーとして使用できるように、K を比較可能であると宣言することが必要です。また、呼び出し側のコードがマップのキーに許容される型を使用することも保証されます。
  • type パラメーターに、とVの 2 つの型の和集合である制約を指定します。Usingは、2 つの型の和集合を指定します。つまり、この制約はどちらの型も許可します。どちらの型も、呼び出しコードの引数としてコンパイラによって許可されます。int64float64|
  • m引数が typeであることを指定します。map[K]Vここで、KV は、型パラメーターに対して既に指定されている型です。は同等の型であるmap[K]Vため、有効なマップ型であることがわかっていることに注意してください。K比較可能なものを宣言していなければK、コンパイラは への参照を拒否しますmap[K]V
fmt.Printf("Generic Sums: %v and %v\n",
    SumIntsOrFloats[string, int64](ints),
    SumIntsOrFloats[string, float64](floats))

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

  • 宣言した汎用関数を呼び出して、作成した各マップを渡します。
  • 型引数 (角括弧内の型名) を指定して、呼び出す関数の型パラメーターを置き換える必要がある型を明確にします。次のセクションで説明するように、関数呼び出しで型引数を省略できることがよくあります。Go は多くの場合、コードからそれらを推測できます。
  • 関数によって返された合計を出力します。

ここまでの内容

package main

import "fmt"

func main() {
	// 整数値用のマップを初期化する
	ints := map[string]int64{
		"first":  34,
		"second": 12,
	}

	// フロート値用のマップを初期化する
	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),
	)
}

// SumIntsはm個の値を足し合わせる。
func SumInts(m map[string]int64) int64 {
	var s int64
	for _, v := range m {
		s += v
	}
	return s
}

// SumFloatsはm個の値を足し合わせる。
func SumFloats(m map[string]float64) float64 {
	var s float64
	for _, v := range m {
		s += v
	}
	return s
}

// SumIntsOrFloatsは、マップmの値を合計します。
// マップの値の型として使用します
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
	var s V
	for _, v := range m {
		s += v
	}
	return s
}

実際にコードを実行してみます

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

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

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

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

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

fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
    SumIntsOrFloats(ints),
    SumIntsOrFloats(floats))
  • このコードでは、次のことを行います。
    • 型引数を省略してジェネリック関数を呼び出します。

コードを実行してみます

$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 つのメソッドで型制約インターフェイスを宣言し、それをジェネリック関数の型パラメーターと共に使用する場合、関数の呼び出しに使用される型引数には、これらのメソッドがすべて含まれている必要があります。

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

type Number interface {
    int64 | float64
}

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

  • Number型制約として使用するインターフェイスの型を宣言します。
  • int64インターフェイス内でandの和集合を宣言float64します。基本的に、共用体を関数宣言から新しい型制約に移動しています。そうすれば、型パラメーターを または のいずれint64かに制約する場合、 を書き出す代わりに、float64この型制約を使用できます。Numberint64 | float64

既にある関数の下に、次の汎用 SumNumbers関数を貼り付けます。

// SumNumbersは,マップmの値を合計する。
// そして、フロート値をマップ値として使用します。
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

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

  • 前に宣言したジェネリック関数と同じロジックでジェネリック関数を宣言しますが、型制約として共用体の代わりに新しいインターフェイス型を使用します。以前と同様に、引数と戻り値の型に型パラメーターを使用します。

main.go で、既にあるコードの下に、次のコードを貼り付けます。

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

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

  • 各マップで呼び出しSumNumbers、それぞれの値から合計を出力します。前のセクションと同様に、ジェネリック関数の呼び出しでは型引数 (角括弧内の型名) を省略します。Go コンパイラは、他の引数から型引数を推測できます。

ここまでのコード

package main

import "fmt"

type Number interface {
	int64 | float64
}

func main() {
	// 整数値用のマップを初期化する
	ints := map[string]int64{
		"first":  34,
		"second": 12,
	}

	// フロート値用のマップを初期化する
	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はm個の値を足し合わせる。
func SumInts(m map[string]int64) int64 {
	var s int64
	for _, v := range m {
		s += v
	}
	return s
}

// SumFloatsはm個の値を足し合わせる。
func SumFloats(m map[string]float64) float64 {
	var s float64
	for _, v := range m {
		s += v
	}
	return s
}

// SumIntsOrFloatsは、マップmの値を合計します。
// マップの値の型として使用します
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
	var s V
	for _, v := range m {
		s += v
	}
	return s
}

func SumNumbers[K comparable, V Number](m map[K]V) V {
	var s V
	for _, v := range m {
		s += v
	}
	return s
}

コードを実行する

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 のジェネリックについて紹介しました。

次のチュートリアルをやっていきましょう!

ぎゅう
WEBエンジニア
渋谷でWEBエンジニアとして働く。
LaravelとVue.jsをよく取り扱い、誰でも仕様が伝わるコードを書くことを得意とする。
先輩だろうがプルリクにコメントをして、リファクタしまくる仕様伝わるコード書くマン
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次
閉じる