Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. Go
    のジェネリクスを活用する
    syumai
    Go
    勉強会 #1 #BuySell_Go (2023/5/24)

    View Slide

  2. 自己紹介
    syumai
    Go Documentation
    輪読会 / ECMAScript
    仕様輪
    読会 主催
    株式会社ベースマキナで管理画面のSaaS
    を開発中
    Go
    でGraphQL
    サーバー (gqlgen)
    や TypeScript

    フロントエンドを書いています
    Twitter: @__syumai
    Website: https://syum.ai

    View Slide

  3. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. 現時点で出来ないこと

    View Slide

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

    View Slide

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

    View Slide

  16. メソッド
    注:
    型パラメータを持つ型に対するメソッドは書くことが出来る
    下記の例の、メソッドのレシーバ名
    Vector[A]

    A
    は、どんな名
    前であってもよい
    type Vector[A any] []A
    // First
    は、 Vector
    の最初の要素を返すメソッド
    func (v Vector[A]) First() A {
    return v[0]
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  24. 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
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  32. ジェネリックにした例
    関数に渡すコールバックの戻り値の型の値がそのまま返るように出来

    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) //
    型アサーションが不要
    }

    View Slide

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

    View Slide

  34. 社内での導入事例

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  42. 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

    View Slide

  43. View Slide

  44. View Slide

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

    View Slide

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

    View Slide

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

    View Slide