Slide 1

Slide 1 text

Go のジェネリクスを活用する syumai Go 勉強会 #1 #BuySell_Go (2023/5/24)

Slide 2

Slide 2 text

自己紹介 syumai Go Documentation 輪読会 / ECMAScript 仕様輪 読会 主催 株式会社ベースマキナで管理画面のSaaS を開発中 Go でGraphQL サーバー (gqlgen) や TypeScript で フロントエンドを書いています Twitter: @__syumai Website: https://syum.ai

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

ベースマキナとは? DB やAPI の接続設定 & 呼び出し設定をするだけで、簡単にUI 生成が行 える管理画面SaaS API 呼び出しへの権限設定や、レビュー依頼 / 承認機能も簡単に使え ます https://about.basemachina.com

Slide 5

Slide 5 text

本日話すこと ジェネリクスの基本についてのおさらい ジェネリクス導入のメリット ジェネリクス導入のテクニック 社内での導入事例

Slide 6

Slide 6 text

ジェネリクスのざっくりとした理解 型定義、関数宣言の型情報の一部をパラメータ化して、使い回せるよ うにする機能 1 つの型定義や、関数宣言をあらゆる型に対して使い回せる 型パラメータの制約に基づいて、パラメータ化された型に対して行 える操作も制限できる 比較演算子の使用、数値の加算など

Slide 7

Slide 7 text

ジェネリクスの基本についてのおさらい

Slide 8

Slide 8 text

型パラメータ 型定義 (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 }

Slide 9

Slide 9 text

型引数 型パラメータに 型引数 (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 }

Slide 10

Slide 10 text

型推論 型引数は、型推論 (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)) // 上の行と同等 }

Slide 11

Slide 11 text

型制約 型パラメータは、型制約 (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 }

Slide 12

Slide 12 text

型制約の書き方 型制約のインタフェースを記述する時には、特別な記法が使える | は、この記号で連結された型を受け付ける ~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 }

Slide 13

Slide 13 text

現時点で出来ないこと

Slide 14

Slide 14 text

型エイリアス 型エイリアスが型パラメータを持てるようにする 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 時点では書けないが、将来的には書けるようになる見込み)

Slide 15

Slide 15 text

メソッド メソッド宣言への型パラメータの追加は、実装上の都合でリリースの 見込みが立っていない 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 ... }

Slide 16

Slide 16 text

メソッド 注: 型パラメータを持つ型に対するメソッドは書くことが出来る 下記の例の、メソッドのレシーバ名 Vector[A] の A は、どんな名 前であってもよい type Vector[A any] []A // First は、 Vector の最初の要素を返すメソッド func (v Vector[A]) First() A { return v[0] }

Slide 17

Slide 17 text

ジェネリクス導入のメリット

Slide 18

Slide 18 text

ジェネリクス導入のメリット 型アサーションを減らせる コード生成を減らせる

Slide 19

Slide 19 text

型アサーションを減らせる ジェネリクス登場前の汎用的な関数の書き方 interface{} を受け付けて、 interface{} を返す 受け取り側で型アサーションを行う

Slide 20

Slide 20 text

ジェネリクス登場前 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 型として使うには、型アサーションが必要 }

Slide 21

Slide 21 text

ジェネリクス登場後 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 型の値として使える }

Slide 22

Slide 22 text

型アサーションを減らせる 汎用の関数や型を作っても、型アサーションを不要に出来るケースが 誕生 コンパイル時点で型が確定するので、型アサーションでは発生する ことのあったruntime panic もなく安全

Slide 23

Slide 23 text

コード生成を減らせる DB やAPI のスキーマからコードを生成するような時に、型部分をパラ メータとして切り出すことで、ある程度コード生成を減らすことが出 来る

Slide 24

Slide 24 text

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 }

Slide 25

Slide 25 text

コード生成を減らせる スキーマに対応する構造体型そのものの生成は無くせないが、 生成さ れた型を使った共通のデータ構造 を個別に再生成する必要はない

Slide 26

Slide 26 text

ジェネリクス導入のテクニック

Slide 27

Slide 27 text

ジェネリクス導入のテクニック まずは型制約をany にしてもいいので入れる ジェネリックにしたいメソッドは関数に書き換える 型推論は積極的に使う

Slide 28

Slide 28 text

まずは型制約をany にしてもいいので入れる 型制約の記法が難しいので、完全に理解してからでないと使えないと 考えがち しかし、実際のユースケースでは複雑な型制約はほとんど要らない まずは、どんな型でも受け付ける any を型制約に設定するところか ら入れていく メンバーにも、同様の内容を伝える // 型制約: any でカバーできるユースケースはかなり多い func F[T any](v T) { ... }

Slide 29

Slide 29 text

まずは型制約を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 }

Slide 30

Slide 30 text

ジェネリックにしたいメソッドは関数に書き換える ジェネリックにしたいメソッドがある時は、 メソッドレシーバを引数 に受け取る関数に書き換える ことで目的を達成できる これはよくあるパターン。メソッドではなくなるので使い勝手が変 わってしまうが、型アサーションが不要になるメリットの方が大き い

Slide 31

Slide 31 text

ジェネリックにしたいメソッドの例 関数の実行結果が、必ず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) }

Slide 32

Slide 32 text

ジェネリックにした例 関数に渡すコールバックの戻り値の型の値がそのまま返るように出来 た 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) // 型アサーションが不要 }

Slide 33

Slide 33 text

型推論は積極的に使う これはテクニックと言うほどの事ではないが、型推論が効くところ ( 特に関数引数型推論) は積極的に使いたい 型引数を明示的に書かなくても、コードの文脈から十分読み取ること ができる 明示的に書くと、読み手が " 明示しないといけなかった理由がある" と考えてしまうので、コードリーディングのコストが上がる可能性 がある Go Team は型推論の優先度を高く設定して対応しているので、直近型 推論が効かない箇所も、じきに効くようになることが期待できる https://github.com/golang/go/issues/46477#issuecomment- 1490490920

Slide 34

Slide 34 text

社内での導入事例

Slide 35

Slide 35 text

社内での導入事例 任意のJSON を返すAPI Client での使用 ライブラリの導入 maps , slices samber/lo

Slide 36

Slide 36 text

任意のJSON を返すAPI Client での使用 レスポンス形式が、リクエストを送る時点で定まっているようなAPI Client で使用している 実際には、リクエストする側がレスポンス形式を指定する機構なの で、かなり特殊なケース

Slide 37

Slide 37 text

ジェネリクス無しの場合 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) }

Slide 38

Slide 38 text

ジェネリクスありの場合 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) // ここでのデコードが不要 }

Slide 39

Slide 39 text

ライブラリの導入 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 との機能の差異は少しある)

Slide 40

Slide 40 text

ライブラリの導入 samber/lo (https://github.com/samber/lo) lodash like なライブラリ slice やmap に対する操作が豊富 pointer に関する操作もある 全部突っ込むと機能が多すぎて逆に迷うので、必要なものだけを選ん で導入している

Slide 41

Slide 41 text

samber/lo の使用例 定数リテラルから直接ポインタを取得したい時 // lo 無し i := 1 p1 := &i // lo あり p2 := lo.ToPtr(1)

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

samber/lo の使用例 注: lo.Map の使用はmake + append の罠の回避に使えるが、lint で防ぐ ことも出来るので、絶対的なメリットとも言い切れない https://github.com/ashanbrown/makezero

Slide 46

Slide 46 text

最後に ジェネリクスが登場してから既に一年が経過 標準ライブラリも充実してきている いよいよ本格的に使われ始める機運が高まっているので、小さいとこ ろから積極的に入れていきましょう

Slide 47

Slide 47 text

ご清聴ありがとうございました!