$30 off During Our Annual Pro Sale. View Details »

Goのジェネリクスを活用する

syumai
May 24, 2023

 Goのジェネリクスを活用する

Go勉強会 #1 #BuySell_Go (2023/5/24)
https://buysell-technologies.connpass.com/event/283768/

syumai

May 24, 2023
Tweet

More Decks by syumai

Other Decks in Programming

Transcript

  1. 自己紹介 syumai Go Documentation 輪読会 / ECMAScript 仕様輪 読会 主催

    株式会社ベースマキナで管理画面のSaaS を開発中 Go でGraphQL サーバー (gqlgen) や TypeScript で フロントエンドを書いています Twitter: @__syumai Website: https://syum.ai
  2. 型パラメータ 型定義 (type definition) と 関数宣言 (function declaration) に、型パ ラメータを持つことが出来る

    // 型定義の例 type Vector[T any] []T // 関数宣言の例 func Min[T constraints.Ordered](a, b T) T { if a < b { return a } return b }
  3. 型引数 型パラメータに 型引数 (type arguments) を渡して、型や関数を イン スタンス化 (instantiation) して使う

    func Min[T constraints.Ordered](a, b T) T { ... } // func Min[int](a, b int) int { ... } // `int` への置き換えイメージ // func Min[float64](a, b float64) float64 { ... } // `float64` への置き換えイメージ func main() { fmt.Println(Min[int](1, 2)) // `T` が 型引数 `int` に置き換わる => 1 fmt.Println(Min[float64](1.5, 0.5)) // `T` が 型引数 `float64` に置き換わる => 0.5 }
  4. 型推論 型引数は、型推論 (type inference) のアルゴリズムによって推論可能 な場合は省略することが出来る。 型推論には 関数引数型推論 (function argument

    type inference) 、 制約型推論 (constraint type inference) の 2 種類が存在し、これら を組み合わせたアルゴリズムで動作する // 関数の引数から型引数を推論する例 func main() { fmt.Println(Min(1, 2)) // `int` が推論される // fmt.Println(Min[int](1, 2)) // 上の行と同等 fmt.Println(Min(1.5, 0.5)) // `float64` が推論される // fmt.Println(Min[float64](1.5, 0.5)) // 上の行と同等 }
  5. 型制約 型パラメータは、型制約 (type constraints) によって受け付ける型引 数を制限することができ、型制約によって行える操作も変わる。 型制約は、インタフェース (interfaces) によって表現される。 //

    fmt.Stringer インタフェースを型制約とする func PrintStringer[T fmt.Stringer](v T) { fmt.Println(v.String()) // ここで String() メソッドを呼べる } type StringableInt int func (i StringableInt) String() string { ... } func main() { PrintStringer(StringableInt(1)) // OK PrintStringer(1) // NG: int does not implement fmt.Stringer }
  6. 型制約の書き方 型制約のインタフェースを記述する時には、特別な記法が使える | は、この記号で連結された型を受け付ける ~T は、T 型を基底型 (underlying type) に持つ型を受け付ける

    type IntOrString1 interface { int | string } // `~` なし type IntOrStrings1[T IntOrString1] []T type IntOrString2 interface { ~int | ~string } // `~` あり type IntOrStrings2[T IntOrString2] []T type MyInt int func main() { _ = IntOrStrings1[MyInt]{1, 2, 3} // NG _ = IntOrStrings2[MyInt]{1, 2, 3} // OK }
  7. 型エイリアス 型エイリアスが型パラメータを持てるようにする Proposal は accept されている https://github.com/golang/go/issues/46477 リリースは Go 1.21

    よりさらに後に持ち越し type Vector[T any] []T type IntVector = Vector[int] // OK ( 型エイリアス宣言は型パラメータを持っていない) type Map[K comparable, V any] map[K]V type StringMap[V any] = Map[string]V // NG (Go 1.18 時点では書けないが、将来的には書けるようになる見込み)
  8. メソッド メソッド宣言への型パラメータの追加は、実装上の都合でリリースの 見込みが立っていない https://go.googlesource.com/proposal/+/refs/heads/master/desi gn/43651-type-parameters.md#No-parameterized-methods type Vector[A any] []A //

    このようなメソッド宣言は書けない func (v Vector[A]) Map[B any](f func(v A) B) Vector[B] { ... } func main() { v := Vector[int]{1, 2, 3} v.Map[string](func (v int) string { ... }).Map ... }
  9. ジェネリクス登場前 type Store map[string]interface{} // 要素型をinterface{} 型にして、任意の型を受け付ける func (s Store)

    Save(key string, value interface{}) { s[key] = value } func (s Store) Load(key string) interface{} { return s[key] } func main() { s := Store{} s.Save("one", 1) one := s.Load("one") // 取得した値は interface{} 型 fmt.Println(1 + one.(int)) // int 型として使うには、型アサーションが必要 }
  10. ジェネリクス登場後 type Store[T any] map[string]T // 要素型を型パラメータ型のT にする func (s

    Store[T]) Save(key string, value T) { s[key] = value } func (s Store[T]) Load(key string) T { return s[key] } func main() { s := Store[int]{} // int 型のStore として明示 s.Save("one", 1) one := s.Load("one") // 取得した値はint 型 fmt.Println(1 + one) // そのままint 型の値として使える }
  11. connect-go の例 connect-go では、Request / Response にProtobuf のMessage 以外の 付加情報

    (Header 等) を含んでいる この情報を付加するための型をそれぞれ生成することなく、 connect.Request や connect.Response でWrap するだけで済ませてい る xxx.ListPostsRequestConnectRequest といった自動生成の型が不要 func (e EmotterServer) ListPosts(ctx context.Context, req *connect.Request[v1.ListPostsRequest]) (*connect.Response[v1.ListPostsResponse], error) { // req.Msg => v1.ListPostsRequest 型の値が型アサーション無しで取れる return connect.NewResponse(&v1.ListPostsResponse{ Posts: loadAllPosts(false), }), nil }
  12. まずは型制約をany にしてもいいので入れる any で足りなくなったら、 comparable 制約か、 exp/constraints package を使う comparable

    は比較可能な型のみを含む。map のキーに使える exp/constraints.Ordered は < 演算子で順序付け可能な型のみを含む これらで足りなかった時に初めて自分で型制約を書けばOK import "golang.org/x/exp/constraints" func Min[T constraints.Ordered](a, b T) bool { if a < b { return a } return b }
  13. ジェネリックにしたいメソッドの例 関数の実行結果が、必ずany になってしまう関数 type Tx struct{ ... } // f

    をTransaction 内で実行して、その結果をそのまま返す関数 func (t *Tx) Do(ctx context.Context, f func (ctx context.Context, tx *sql.Tx) (any, error)) (any, error) { ... } func main() { ctx := context.Background() t := Tx{ ... } result, err := t.Do(ctx, func (ctx context.Context, tx *sql.Tx) (any, error){ ... return &model.User{ ... }, nil }) if err != nil { ... } u, ok := result.(*model.User) // 型アサーションが必要 if !ok { ... } fmt.Println(u.Name) }
  14. ジェネリックにした例 関数に渡すコールバックの戻り値の型の値がそのまま返るように出来 た type Tx struct{ ... } // *Tx

    を引数として組み替えて、戻り値を型パラメータT にする func Do[T any](ctx context.Context, t *Tx, f func (ctx context.Context, tx *sql.Tx) (T, error)) (T, error) { ... } func main() { ctx := context.Background() t := Tx{ ... } // *model.User を戻り値の型に出来る & 型推論が効くので、型引数の指定が不要 result, err := Do(ctx, t, func (ctx context.Context, tx *sql.Tx) (*model.User, error){ ... return &model.User{ ... }, nil }) if err != nil { ... } fmt.Println(u.Name) // 型アサーションが不要 }
  15. 型推論は積極的に使う これはテクニックと言うほどの事ではないが、型推論が効くところ ( 特に関数引数型推論) は積極的に使いたい 型引数を明示的に書かなくても、コードの文脈から十分読み取ること ができる 明示的に書くと、読み手が " 明示しないといけなかった理由がある"

    と考えてしまうので、コードリーディングのコストが上がる可能性 がある Go Team は型推論の優先度を高く設定して対応しているので、直近型 推論が効かない箇所も、じきに効くようになることが期待できる https://github.com/golang/go/issues/46477#issuecomment- 1490490920
  16. ジェネリクス無しの場合 package jsonapi type Client struct { ... } //

    io.Reader を返して、結果のデコードは使用者に任せる func (c *Client) Execute(ctx context.Context, req any) (io.Reader, error) { ... } // --- package main type EchoRequest struct { Message string } type EchoResponse struct { Message string } func main() { ctx := context.Background() c := &jsonapi.Client{ ... } rd, err := c.Execute(ctx, &EchoRequest{ Message: "Hello!" }) if err != nil { ... } var res EchoResponse // ここでJSON decode しないといけない if err := json.NewDecoder(rd).Decode(&res); err != nil { ... } fmt.Println(res) }
  17. ジェネリクスありの場合 package jsonapi type Client struct { ... } //

    デコード済みの値を返す func Execute[T any](ctx context.Context, c *Client, req any) (T, error) { ... var res T // ここでJSON decode できる if err := json.NewDecoder(rd).Decode(&res); err != nil { ... } return res, nil } // --- package main type EchoRequest struct { Message string } type EchoResponse struct { Message string } func main() { ctx := context.Background() c := &jsonapi.Client{ ... } res, err := jsonapi.Execute[EchoResponse](ctx, c, &EchoRequest{ Message: "Hello!" }) if err != nil { ... } fmt.Println(res) // ここでのデコードが不要 }
  18. ライブラリの導入 slices / maps golang.org/x/exp 配下に置かれている golang.org/x/exp/slices golang.org/x/exp/maps slices slices.Contains

    / Equal / Compare / Sort などがある maps maps.Keys / Values / Equal などがある これらは既にexp から標準ライブラリへの移管が決まっており、 Go 1.21 で追加される見込み (exp との機能の差異は少しある)
  19. ライブラリの導入 samber/lo (https://github.com/samber/lo) lodash like なライブラリ slice やmap に対する操作が豊富 pointer

    に関する操作もある 全部突っ込むと機能が多すぎて逆に迷うので、必要なものだけを選ん で導入している
  20. samber/lo の使用例 slice の要素をMap して変換したい時 // samber/lo のサンプルから引用 list :=

    []int64{1, 2, 3, 4} result := lo.Map(list, func(nbr int64, index int) string { return strconv.FormatInt(nbr*2, 10) }) fmt.Printf("%v", result) https://go.dev/play/p/OkPcYAhBo0D