Slide 1

Slide 1 text

isugata ベンチマーカーのための カッコいいHTTPレスポンスバリデーターを作る logica X(旧Twitter): @logic0419 GitHub: logica0419 Zenn: logica0419

Slide 2

Slide 2 text

軽く自己紹介 logicaです traQ(traPで使われてるメッセージングサービス)の バックエンドメンテナンスリーダーをしてます ISUCON歴 ISUCON11: 予選敗退 ISUCON12: 学生枠で決勝進出、13位 ISUCON13: 所用で残念ながら欠席 「作問運営になりたい!」と公言している少数民族です ISUCON夏祭りでオリジナルISUCON問題を公開しました

Slide 3

Slide 3 text

ベンチマーカーの超大雑把な仕組み 負荷をかける + 正しく処理できているか確かめる 「ISUCONのベンチマーカーは、e2eテストを兼ねる」と言われる 1サイクルはとても単純 リクエストを送る → レスポンスのバリデーション このサイクルを、大量にかつ同時に回すと 勝手にISUCONのベンチマーカーになる

Slide 4

Slide 4 text

HTTPレスポンスのバリデーション Goの *http.Response の、よく確認されるとこだけ抜き出す type Response struct { Status string StatusCode int Header Header Body io.ReadCloser ContentLength int64 } …意外と愚直にやれるのでは?

Slide 5

Slide 5 text

愚直なバリデーション 例えばこんな風にできそう var res *http.Response if res.StatusCode != 200 { return errors.New("status code is wrong") } var body User _ = json.NewDecoder(res.Body).Decode(&body) if body.ID != 1 { return errors.New("body is wrong") } 大量に書くとなるとちょっとめんどくさいかも 共通のロジックをいろんなとこに書くのも微妙

Slide 6

Slide 6 text

ユーティリティー化されたバリデーション ちょっとユーティリティー化してみる func ValidateStatusCode(res *http.Response, code int) error { if res.StatusCode != code { return errors.New("status code is wrong") } return nil } var res *http.Response if err := ValidateStatusCode(res, 200); err != nil { return err } ちょっとスマートに書けるようになった

Slide 7

Slide 7 text

ユーティリティー化されたバリデーション レスポンスのありとあらゆる部分がバリデーション対象なので、 毎回以下のようになる err := ValidateStatusCode(res, 200) err := ValidateContentType(res, "application/json") var body User _ = json.NewDecoder(res.Body).Decode(&body) if body.ID != 1 { ~~ } 毎回res渡すのめんどくさいし危ない resが途中でどうなるか保証がない Bodyに対するバリデーションは共通化しにくい Bodyに対する共通処理がどこかで抜けるかも

Slide 8

Slide 8 text

なによりバリデーションの見た目が カッコよくない! 見た目がカッコいいと、モチベが上がる ベンチマーカーは割と執念の産物なので、 モチベが上がるのは意外と重要

Slide 9

Slide 9 text

解決したい問題 *http.Response をいろんな関数に渡すのをやめたい できれば1つの関数で完結させたい Bodyに関する処理をユーティリティー化したい Bodyに対する共通処理をどこかでミスる可能性をなくしたい Bodyを読み切らないとkeep-aliveが効かない問題があって、 必ずBodyを読み切る処理を加えたい 見た目がカッコよくない (主観) これらに対する解決策が…

Slide 10

Slide 10 text

Functional Option Pattern 「ある構造体に対するOption」の実装パターン Optionを「その構造体を引数に取る関数」として定義 全てのOptionに構造体を1度ずつ通すことで設定を適用する 利点 様々なOptionを自由な組み合わせで取れる いくらでも拡張可能・使わない選択も可能 カッコいい!!!!!!オシャレ!!!!!!! 欠点 実装がちょっと長い

Slide 11

Slide 11 text

Functional Option Pattern type Server struct { // 色んなフィールド } type Option func(*Server) error func NewServer(opts ...Option) (*Server, error) { s := &Server{} for _, opt := range opts { if err := opt(s); err != nil { return nil, err } } return s, nil }

Slide 12

Slide 12 text

Functional Option Pattern Optionを「その構造体を引数に取る関数」として定義 全てのOptionに構造体を1度ずつ通すことで設定を適用する バリデーターも、 *http.Response を1回ずつ通すなぁ… 様々なOptionを自由な組み合わせで取れる いくらでも拡張可能・使わない選択も可能 カッコいい!!!!!!オシャレ!!!!!!! …バリデーターに活かしたら最高に面白いことになるのでは?

Slide 13

Slide 13 text

というわけでやっちゃいました 詳細はぜひリンクから… https://github.com/logica0419/isugata https://pkg.go.dev/github.com/logica0419/isugata

Slide 14

Slide 14 text

簡単に紹介 基幹になるのは以下二つ type ValidateOpt func(*http.Response) error func Validate(res *http.Response, opts ...ValidateOpt) error しっかりFunctional Option Patternが活かされている resを全てのoptsに通すことでバリデーションを行う 自分でValidateOpt型の関数を作ることで、 オリジナルのバリデーションを行うことも可能

Slide 15

Slide 15 text

メタデータ系 func WithStatusCode(code int) ValidateOpt { return func(res *http.Response) error { // 割愛 } } func WithContentType(contentType string) ValidateOpt { return func(res *http.Response) error { // 割愛 } } ValidateOpt を生成する関数を定義することで、任意の値を照合する

Slide 16

Slide 16 text

JSON Body系 type JSONValidateOpt[V any] func(body V) error func WithJSONValidation[V any]( opts ...JSONValidateOpt[V] ) ValidateOpt { return func(res *http.Response) error { // デコードとoptsの実行 } } ジェネリクスを使って、様々な構造体のバリデーションを まとめることに成功 Array(配列)型は、長さなどのバリデーションがしたかったため 別に JSONArrayValidateOpt 型を用意(中身はほぼ一緒)

Slide 17

Slide 17 text

どうだ!カッコいいだろ!!! err := isugata.Validate(res, isugata.WithStatusCode(http.StatusOK), isugata.WithContentType("application/json"), isugata.WithJSONArrayValidation( isugata.JSONArrayLengthEquals[user](2), isugata.JSONArrayValidateOrder( func(u user) int { return u.ID }, isugata.Asc, ), isugata.JSONArrayValidateEach( func(body user) error { if body.Name != fmt.Sprintf("test%d", body.ID) { return errors.New("body is wrong") } return nil }, ), ), )

Slide 18

Slide 18 text

ありがとうございました みなさんも是非、Functional Option Pattern使ってみて下さいね 参考文献 Go言語のFunctional Option Pattern (https://qiita.com/weloan/items/56f1c7792088b5ede136)