
はじめまして!
Liquidのバックエンドエンジニアの谷口と申します。(画像左)
GoでLiquid eKYCや新規プロダクトの開発を行っています。
ジェネリクスのproposalがacceptされたので、調べてみました。
今回解説するproposalの範囲
このproposalではtype list
という概念を使って、ジェネリクスを実現しています。
さらに、これを改良する新しいproposalも出されています。(https://github.com/golang/go/issues/45346)
type list
を導入するのではなく、type set
という新しい概念の導入によって、ジェネリクスを実現しようというものです。
これらの2つのproposalを理解するには下記の記事が非常に参考になります。
Go の "Type Sets" proposal を読む
type list
ではなく、type set
が導入されると仮定して、どのようにeKYCに活用できるか考えてみます。
Goにジェネリクスが導入されると、下記のようなコードを記述できるようになります。
これはどちらのproposalでも変わりません。
type A[T any] []T
func B[T any](p T) { fmt.Println(p) }
type Constraint interface{ Do() }
func C[T Constraint](p T) { p.Do() }
any
は新しく導入される予約語です。どんな型でも受け付けます。
上記のT
には型制約がついているので、これを実装した型以外を渡すとコンパイルエラーになります。
型制約はinterface
でなければなりません。
それぞれ下記のように使用します。
var m A[int] = []int{1, 2, 3}
B[int](1)
type cImplemented int
func (c cImplemented) Do() { fmt.Println(c) }
C[cImplemented](cImplemented(2))
go2go Playground
型制約とtype list
従来のinterface
しか型制約として使用できないと困るケースがあります。
+
、<
といった演算ができません。
func D[T Comparable](s T) {
result := s[0] > s[1]
fmt.Prinltn(result)
}
そのため、accept済みのproposalではtype list
という概念を導入して、これを解決しています。
type Comparable interface {
type int
}
これでint
をunderlying type
に持つ型を使用できるようになります。
The Go Playground
underlying type
のついては下記の資料が参考になります。
入門Go言語仕様 Underlying Type / Go Language Underlying Type
簡単に説明すると、下記のような型および、int
がint
のunderlying type
です。
type MyInt int
type MyMyInt MyInt
type MyMyMyInt MyMyInt
下記のように複数記述することで、いずれかの型をunderlying type
に持つ型を使用できます。
type Comparable interface {
type int, int8, int16, int32, int64
}
type listではなくtype set
新しいproposalでは、type list
は無くなり、type set
という概念を導入しています。
下記のように記述します。
type Comparable interface {
int
}
int
型のみしか使用できなくなります。
underlying type
は使用できません。
type MyInt int
underlying type
を使用するには下記のように記述します。
type Comparable interface {
~int
}
type MyInt int
複数の型を許容したい場合は下記のように記述します。
type Comparable interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
使用例
エンティティを表現した構造体のsliceからIDを全件取得したい
エンティティを表現するときには構造体を使用すると思います。
このような構造体のsliceからIDを全件取得するメソッドをそれぞれのsliceにすべて記述するのは結構面倒です。
type A struct {
ID int
FieldA string
FieldB string
FieldC string
}
type As []*A
func (as As) IDs() []int {
ids := make([]int, 0, len(as))
for _, a := range as {
ids = append(ids, a.ID)
}
return ids
}
type B struct {
ID int
FieldD string
}
type Bs []*B
func (bs Bs) IDs() []int {
ids := make([]int, 0, len(bs))
for _, a := range bs {
ids = append(ids, a.ID)
}
return ids
}
func main() {
fmt.Println(As{{ID: 1}, {ID: 2}, {ID: 3}}.IDs())
fmt.Println(Bs{{ID: 1}, {ID: 2}, {ID: 3}}.IDs())
}
The Go Playground
これはジェネリクスを使用して下記のように修正できるでしょう。
type Entity interface {
PrimaryKey() int
}
func PluckID[T Entity](es []T) []int {
ids := make([]int, 0, len(es))
for _, e := range es {
ids = append(ids, e.PrimaryKey())
}
return ids
}
type A struct {
ID int
FieldA string
FieldB string
FieldC string
}
func (a *A) PrimaryKey() int { return a.ID }
type B struct {
ID int
FieldD string
}
func (b *B) PrimaryKey() int { return b.ID }
func main() {
fmt.Println(PluckID([]*A{{ID: 1}, {ID: 2}, {ID: 3}}))
fmt.Println(PluckID([]*B{{ID: 1}, {ID: 2}, {ID: 3}}))
}
The go2go Playground
同じようなメソッドを書く必要がなくなるのでバグが減りそうです。
type set
が下記のような制約を許すようになれば、PrimaryKey
メソッドすら実装不要になるでしょう。
type Fooer interface {
~struct { Foo int; Bar string; ... }
}
type MyFoo struct {
Foo int
Bar string
Baz float64
}
しかし、これを質問している方がすでにおり、回答を見る限り、今回のproposalには含まれなさそうです。
https://github.com/golang/go/issues/45346#issuecomment-812670939
I'm curious whether it would be possible to allow approximations of structs to match not only the underlying type of a particular struct, but also to allow matches for structs that have at least the exact fields that are listed as the approximate struct element?
https://github.com/golang/go/issues/45346#issuecomment-812661004
Thanks, I think this is an idea for later, likely with a different syntax. I don't think we want to overload the ~ syntax just for struct types.
ポインタに変換するだけのメソッド
構造体をjsonにエンコードする際、特定のフィールドに対してnullを許容したいときポインタを使用することがあると思います。
type Response struct {
A *int `json:"a"`
B *string `json:"b"`
C *bool `json:"c"`
}
func main() {
a := 1
b := true
res := &Response{
A: &a,
B: nil,
C: &b,
}
buf := bytes.NewBuffer(nil)
json.NewEncoder(buf).Encode(res)
fmt.Println(buf.String())
}
The Go Playground
a = 1
のように一時変数に代入するのを避けるには
それぞれの型に対してメソッドを定義する必要があります。
func uintPtr(uint v) *uint {return &v }
func intPtr(int v) *int {return &v }
func strPtr(string v) *string {return &v }
ジェネリクスを使用することで、これらのメソッドを一つにまとめられます。
func Ptr[T any](v T) *T {
return &v
}
type Response struct {
A *int `json:"a"`
B *string `json:"b"`
C *bool `json:"c"`
}
func main() {
res := &Response{
A: Ptr(1),
B: nil,
C: Ptr(true),
}
buf := bytes.NewBuffer(nil)
json.NewEncoder(buf).Encode(res)
fmt.Println(buf.String())
}
The go2go Playground
感想
コピペで作られがちなコードをまとめたり、interface{}
で書いている処理を型安全に書けるようになるので良いと感じました。
複数型に対応した処理を自動生成するケースでも、かなりの量をジェネリクスにリライトできると思います。
リリースが楽しみですね。
以上です。