LoginSignup
2
2

はじめに

今回はGoのジェネリクスの使い方をまとめました。
スライドもあります。

any

anyとは

anyとは空インターフェイスinterface{}のエイリアスで、どんな型でも保持することが可能。

以下は実際にanyにいろんな型を入れてみた例。

Go Playground

func main() {
	var i any
	i = 42 // int
	i = "foo" // string

	i = struct {
		s string
	}{
		s: "bar",
	} // struct

	i = func() {} //関数

	_ = i
}

anyのデメリット

  • 過剰な一般化
  • 静的型付け言語の利点が失われる

anyは何の情報も持たないので基本的には使わない方がいい。

以下のようにanyを使うと、anyの引数を受け取り、anyを返すので、メソッドの表現力に欠ける。また、型検査がないので、危ない。

type A struct {}
type B struct {}
type C struct {}
func (c *C) Get(id string)(any, error) {}
func (c *C) Set(id string, v any) error {}

Goは静的型付け言語であるので、Goの利点が失われる。
Goではシグニチャをできるだけ明示的にする。

func (c *C) GetContract(id string) (Contract, error) {}
func (c *C) SetContract(id string, contract Contract) error {}
func (c *C) GetCustomer(id string) (Contract, error) {}
func (c *C) SetCustomer(id string, contract Contract) error {}

また、クライアントはinterfaceで以下のように抽象化を行える。

type ContractC interface {
    GetContract(id string) (c.Contract, error)
    SetContract(id string, c.Contract) error
}

anyを使うべきとき

マーシャル関数など、あらゆる型を受け取ったり返したりする必要があるときは、anyを用いる。

以下は、標準ライブラリから、関数やメソッドがanyの引数を受け取る例。

encoding/jsonのMarshal

func Marshal(v any) ([]byte, error) {
	e := newEncodeState()
	defer encodeStatePool.Put(e)

	err := e.marshal(v, encOpts{escapeHTML: true})
	if err != nil {
		return nil, err
	}
	buf := append([]byte(nil), e.Bytes()...)

	return buf, nil
}

ただし、interface同様、過剰に一般化させるのは避けるべきである。

database/sqlのQueryContextでもanyが使用されている。

func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) {
	var rows *Rows
	var err error

	err = db.retry(func(strategy connReuseStrategy) error {
		rows, err = db.query(ctx, query, args, strategy)
		return err
	})

	return rows, err
}

ジェネリクス

ジェネリクスを使用すれば、anyによる過剰な一般化を避けることができます。

ジェネリクスを使用しない例

以下の例は、map型からキーを全て出力する。

Go Playground

func main() {
	m := map[string]int{
		"太郎": 25,
		"次郎": 30,
		"花子": 35,
	}
	keys := getKeys(m)
	fmt.Print(keys) // [太郎 次郎 花子]
}

func getKeys(m map[string]int) []string {
	var keys []string
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

この例では、map[string]intではなく、map[int]stringなどキーと値が他の型であった場合に動かなくなる。

anyで実装した例

Go Playground

anyを利用して、以下のようにキーと値がどのような型でも受け取れるようにする。

func getKeys(m any) []any {
	switch t := m.(type) {
	default:
		return nil
	case map[string]int:
		var keys []any
		for k := range t {
			keys = append(keys, k)
		}
		return keys
	case map[int]string:
		var keys []any
		for k := range t {
			keys = append(keys, k)
		}
		return keys
	case map[int]int:
		return nil
	}
}

anyを使って実装すると、getKeysが異なる型で同じ処理をしたい場合、同じコードが増えてしまう。これはDRY原則に反する。また、anyを受けとり、anyを返すので、Goの静的型付け言語の利点が失われている。

comparableを使う例

インターフェイス同様anyによる無駄な抽象化は避けるべきである。
mapでは、var m map[[]byte]intのように、keyにスライスを取ることはできない。keyをany型の代わりにcomparable を用いて以下のようにする。Go Playground

func getKeys[K comparable, V any](m map[K]V) []K {
	var keys []K
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

T comparableとは、組み込みのインターフェイス。T comparable==!= で比較可能な値のみ引数に受け取ることが可能になる。

独自の型を使う

unionsを用いて、インターフェイスで独自の型制約を定義することもできる。Go playground

type customConstraint interface {
	~int | ~string
}

func getKeys[K customConstraint, V any](m map[K]V) []K {
	var keys []K
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

ちなみに、unionsを含むインタフェースは型制約でしか使えません。var x customConstraintのようにはできない。

ジェネリクスの注意

メソッドでは使用できない。(関数は可能)

type Foo struct {}

// ./main.go:29:15: methods cannot have type parameters
func (Foo) bar[T any](t T) {}

structでは使用できる。

type Node[T any] struct {
   Val  T
   next *Node[T]
}

func (n *Node[T]) Add(next *Node[T]) {
   n.next = next
}

以下は、独自の型制約をstructに持たせた例
Go Playground

func main() {
	taro := Person[int]{age: 10}
	taro.f()
	jiro := Person[string]{age: "ten"}
	jiro.f()
}

type T interface {
	~int | ~string
}

type Person[T any] struct {
	age T
}

func (p Person[T]) f() {
	fmt.Println(p.age)
}

interfaceにstrcutを埋め込むこともできる。しかし、フィールドにはアクセスできない。

type I interface {
	Person
}

type Person struct {
	age  int
	name string
}

func f[T I](x T) {
	fmt.Printf("%v", x)  // x.nameにするとコンパイルエラー
}

func main() {
	x := Person{age: 10, name: "Taro"}
	f(x)
}

以下のようにしても同様にコンパイルエラー。

func f[T Person](x T) {
	fmt.Printf("%v", x.name)
}

ジェネリクスはあくまでも型制約のみで、実際にフィールドにアクセスすることはできない。

~int v.s. int

~intはintをベースとする型を全て受け取れる。

Go Playground

type CustomInt int

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

func main() {
	var a, b int = 1, 2
	fmt.Println(Add(a, b)) // 3

	var c, d CustomInt = 3, 4
	fmt.Println(Add(c, d)) // 7
}

使用すべきとき

  • データ構造(e.g. バイナリツリー、連結リスト、ヒープ)
  • スライス、マップ、および任意のタイプのチャネルで動作する関数
func merge[T any](ch1, ch2 <-chan T) <-chan T {
    // ...
}
  • ジェネリクスを使って振る舞いを抽象化する

以下はsortパッケージでの例

type sliceFn[T any] struct {
   s       []T
   compare func(T, T) bool
}

func (s sliceFn[T]) Len() int           { return len(s.s) }
func (s sliceFn[T]) Less(i, j int) bool { return s.compare(s.s[i], s.s[j]) }
func (s sliceFn[T]) Swap(i, j int)      { s.s[i], s.s[j] = s.s[j], s.s[i] }

ジェネリクスを使用すべきではないとき

  • 受け取った引数のメソッドを使用するとき
func foo[T io.Writer](w T) {
   b := getBytes()
   _, _ = w.Write(b)
}
  • ジェネリクスを使うことでコードが複雑になるとき
    • Goの開発者は10年間ジェネリクスなしでやってきた

参考文献

  1. Being confused about when to use generics (#9) - 100 Go Mistakes and How to Avoid Them
  2. Type Parameters Proposal
  3. Go言語のジェネリクス入門
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2