Share
Embed
Go API クライアントの実装 〜Go Conference に載せれなかったTIPS〜
\非公式/ Go Conference 2022 Spring スポンサー企業4社 アフタートーク
2022年4月28日
Yoshiki Nakagawa
April 30, 2022
Transcript
23 slides
-
© 2022 LayerX Inc. 1 Go API クライアントの実装 〜Go Conference
に載せれなかったTIPS〜 \非公式/ Go Conference 2022 Spring スポンサー企業4社 アフタートーク 2022.04.28 -
© 2022 LayerX Inc. 2 自己紹介 株式会社LayerX 中川佳希 @yyoshiki41 バクラク請求書のテックリード
バックエンドからフロントエンドまで. SaaS が扱う業務ドメインへの好奇心と サービスの成長に日々ワクワクを感じています. -
© 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^)/ -
© 2022 LayerX Inc. 4 今日の発表 4月23日(土)Go Conference 2022 Spring
Go API クライアントの実装 1. 認証 2. APIコールの実装 3. エラーハンドリング 4. テスト 今日は、登壇時に載せきれなかったテクニックを紹介していきます。 -
© 2022 LayerX Inc. 5 1. Logger 2. Functional Option
Pattern 3. デバッグ 4. アンチパターン a. init b. DELETE メソッド Agenda -
© 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 } -
© 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()) -
© 2022 LayerX Inc. 8 複数のパラメータがあり, 全てが必須な引数でない場合などに有効. ボディパラメータのオブジェクトに, キーなどが自由に設定できるAPI を想定.
例 Functional Option { "email": "[email protected]", "custom_flag": true, "custom_key": "value" } -
© 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 } } -
© 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 -
© 2022 LayerX Inc. 11 httputil.DumpResponse 受け取ったHTTPレスポンスを出力するデバッグ関数 HTTPステータスコード, ホスト名, ヘッダー情報,
ボディパラメータなどが出力される デバッグ func DumpResponse(resp *http.Response, body bool) ([]byte, error) -
© 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) -
© 2022 LayerX Inc. 13 httputil.DumpRequestOut 実行したリクエストのパラメータを出力するデバッグ関数 HTTPメソッド, ホスト名, ヘッダー情報,
ボディパラメータなどが出力される デバッグ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) -
© 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) -
© 2022 LayerX Inc. 15 httputil.DumpRequestOut vs httputil.DumpRequest > DumpRequestOut
is like DumpRequest but for outgoing client requests. httputil.DumpRequestOut => Outgoing Request => クライアント側で, 送信するリクエストのデバッグ用 httputil.DumpRequest => サーバー側で, 受け取ったリクエストのデバッグ用 デバッグ -
© 2022 LayerX Inc. 16 ライブラリの init 関数 実行環境や外部からのインジェクションなどへの対応が難しくなる. アプリからみると,
暗黙的な関数実行が行われる. アンチパターン type Config struct { ... } var _config Config func init() { _config = Config{} } -
© 2022 LayerX Inc. 17 ライブラリの init 関数 init 関数は避けて,
関数化を考える. アンチパターン func NewConfig(args …) Config { return Config{ … } } func NewDefaultConfig() Config { return NewConfig(...) } -
© 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. アンチパターン -
© 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) -
© 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 -
© 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) -
© 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)