はじめに
今回はGoのジェネリクスの使い方をまとめました。
スライドもあります。
any
anyとは
anyとは空インターフェイスinterface{}
のエイリアスで、どんな型でも保持することが可能。
以下は実際にanyにいろんな型を入れてみた例。
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の引数を受け取る例。
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型からキーを全て出力する。
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で実装した例
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をベースとする型を全て受け取れる。
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年間ジェネリクスなしでやってきた