Slide 1

Slide 1 text

© 2022 LayerX Inc. 1 Go API クライアントの実装 〜Go Conference に載せれなかったTIPS〜 \非公式/ Go Conference 2022 Spring スポンサー企業4社 アフタートーク 2022.04.28

Slide 2

Slide 2 text

© 2022 LayerX Inc. 2 自己紹介 株式会社LayerX 中川佳希 @yyoshiki41 バクラク請求書のテックリード バックエンドからフロントエンドまで. SaaS が扱う業務ドメインへの好奇心と サービスの成長に日々ワクワクを感じています.

Slide 3

Slide 3 text

© 2022 LayerX Inc. 3 最近のイベント 4月20日(水)SaaS.tech #2 マルチテナントのアプリケーション実装 〜実践編〜 4月23日(土)Go Conference 2022 Spring Go API クライアントの実装 4月28日(木)今日 \非公式/ Go Conference 2022 Spring スポンサー企業4社 アフタートーク 乗り切ればGW \(^o^)/

Slide 4

Slide 4 text

© 2022 LayerX Inc. 4 今日の発表 4月23日(土)Go Conference 2022 Spring Go API クライアントの実装 1. 認証 2. APIコールの実装 3. エラーハンドリング 4. テスト 今日は、登壇時に載せきれなかったテクニックを紹介していきます。

Slide 5

Slide 5 text

© 2022 LayerX Inc. 5 1. Logger 2. Functional Option Pattern 3. デバッグ 4. アンチパターン a. init b. DELETE メソッド Agenda

Slide 6

Slide 6 text

© 2022 LayerX Inc. 6 Logger インターフェイスを定義しておく アプリ側での拡張したロギング設定が使える // Logger generic interface for logger type Logger interface { Printf(string, ...interface{}) } // 複数の Severity を持たせる場合 type LeveledLogger interface { Info(string, ...interface{}) Warn(string, ...interface{}) Error(string, ...interface{}) } Logger // Config is a setting for 3rd Party APIs. type Config struct { … Log Logger } func (c *Config) SetLogger(l Logger) { c.Log = l }

Slide 7

Slide 7 text

© 2022 LayerX Inc. 7 ライブラリ側でのログ Logger response, err := httpClient.Do(req) if err != nil { … } defer response.Body.Close() c.logf("[3rd Party API] %s: %v %v", response.Status, req.Method, req.URL.String())

Slide 8

Slide 8 text

© 2022 LayerX Inc. 8 複数のパラメータがあり, 全てが必須な引数でない場合などに有効. ボディパラメータのオブジェクトに, キーなどが自由に設定できるAPI を想定. 例 Functional Option { "email": "[email protected]", "custom_flag": true, "custom_key": "value" }

Slide 9

Slide 9 text

© 2022 LayerX Inc. 9 Functional Option type Properties map[string]interface{} type Prop func(*Properties) func PropEmail(v interface{}) Prop { return func(args *Properties) { (map[string]interface{})(*args)["email"] = v } } func PropCustom(k string, v interface{}) Prop { return func(args *Properties) { (map[string]interface{})(*args)[k] = v } }

Slide 10

Slide 10 text

© 2022 LayerX Inc. 10 アプリからの呼び出し ctx := context.Background() resp, err := client.Create( ctx, api.PropEmail("[email protected]"), api.PropCustom("custom_flag", true), api.PropCustom("custom_key", "value"), ) if err != nil { log.Fatal(err) } Functional Option

Slide 11

Slide 11 text

© 2022 LayerX Inc. 11 httputil.DumpResponse 受け取ったHTTPレスポンスを出力するデバッグ関数 HTTPステータスコード, ホスト名, ヘッダー情報, ボディパラメータなどが出力される デバッグ func DumpResponse(resp *http.Response, body bool) ([]byte, error)

Slide 12

Slide 12 text

© 2022 LayerX Inc. 12 httputil.DumpResponse デバッグ resp, err := http.Get(ts.URL) if err != nil { log.Fatal(err) } defer resp.Body.Close() dump, err := httputil.DumpResponse(resp, true) if err != nil { log.Fatal(err) } fmt.Printf("%q", dump)

Slide 13

Slide 13 text

© 2022 LayerX Inc. 13 httputil.DumpRequestOut 実行したリクエストのパラメータを出力するデバッグ関数 HTTPメソッド, ホスト名, ヘッダー情報, ボディパラメータなどが出力される デバッグ func DumpRequestOut(req *http.Request, body bool) ([]byte, error)

Slide 14

Slide 14 text

© 2022 LayerX Inc. 14 httputil.DumpRequestOut デバッグ const body = "Go is a general-purpose language designed with systems programming in mind." req, err := http.NewRequest("PUT", "http://www.example.org", strings.NewReader(body)) if err != nil { log.Fatal(err) } dump, err := httputil.DumpRequestOut(req, true) if err != nil { log.Fatal(err) } fmt.Printf("%q", dump)

Slide 15

Slide 15 text

© 2022 LayerX Inc. 15 httputil.DumpRequestOut vs httputil.DumpRequest > DumpRequestOut is like DumpRequest but for outgoing client requests. httputil.DumpRequestOut => Outgoing Request => クライアント側で, 送信するリクエストのデバッグ用 httputil.DumpRequest => サーバー側で, 受け取ったリクエストのデバッグ用 デバッグ

Slide 16

Slide 16 text

© 2022 LayerX Inc. 16 ライブラリの init 関数 実行環境や外部からのインジェクションなどへの対応が難しくなる. アプリからみると, 暗黙的な関数実行が行われる. アンチパターン type Config struct { ... } var _config Config func init() { _config = Config{} }

Slide 17

Slide 17 text

© 2022 LayerX Inc. 17 ライブラリの init 関数 init 関数は避けて, 関数化を考える. アンチパターン func NewConfig(args …) Config { return Config{ … } } func NewDefaultConfig() Config { return NewConfig(...) }

Slide 18

Slide 18 text

© 2022 LayerX Inc. 18 DELETE メソッド (Go に限った話ではありませんが)DELETE メソッドのAPI エンドポイントへは基本的に ボディペイロードを投げない. ※ 仮にボディペイロードが要求されているなら, Content-Length を忘れずに! (サーバー側でボディの読み出しが行えない) ちなみに, POST/PUT は Content-Length ヘッダー必須. DELETE は任意.. c.f. RFC7231 の DELETE セクション A payload within a DELETE request message has no defined semantics; sending a payload body on a DELETE request might cause some existing implementations to reject the request. アンチパターン

Slide 19

Slide 19 text

© 2022 LayerX Inc. 19 DELETE メソッド 実際にハマった例 nil (ボディペイロードなし) でリクエストを送ったつもりだったが, httputil.DumpRequestOut でログを吐いてみると.. アンチパターン var body io.Reader jsonParams, err := json.Marshal(nil) if err != nil { return err } body = bytes.NewBuffer(jsonParams) req, err := http.NewRequest(http.MethodDelete, ts.URL, body)

Slide 20

Slide 20 text

© 2022 LayerX Inc. 20 DELETE メソッド null として, ボディペイロードを送ってしまっていた. そのため, API 側からは BadRequest が返された.(DELETE でのボディペイロードを 受け付けない) アンチパターン DELETE / HTTP/1.1 Host: 127.0.0.1:63318 User-Agent: Go-http-client/1.1 Content-Length: 4 Accept-Encoding: gzip null

Slide 21

Slide 21 text

© 2022 LayerX Inc. 21 DELETE メソッド 対処法:nil チェックなどで, ボディペイロードなしになるようにチェック アンチパターン var body io.Reader if postPayload != nil { jsonParams, err := json.Marshal(postPayload) if err != nil { return } body = bytes.NewBuffer(jsonParams) } req, err := http.NewRequest(http.MethodDelete, ts.URL, body)

Slide 22

Slide 22 text

© 2022 LayerX Inc. 22 DELETE メソッド 対処法:nil チェックなどで, ボディペイロードなしになるようにチェック その後、httputil.DumpRequestOut でログを取る. 意図した Outgoing Request に \(^o^)/ アンチパターン DELETE / HTTP/1.1 Host: 127.0.0.1:63420 Accept-Encoding: gzip User-Agent: Go-http-client/1.1 dump, err := httputil.DumpRequestOut(req, true)

Slide 23

Slide 23 text

© 2022 LayerX Inc. 23 Have a Nice GW \(^o^)/ まとめ