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

Go API クライアントの実装 〜Go Conference に載せれなかったTIPS〜

Go API クライアントの実装 〜Go Conference に載せれなかったTIPS〜

\非公式/ Go Conference 2022 Spring スポンサー企業4社 アフタートーク
2022年4月28日

Yoshiki Nakagawa

April 30, 2022
Tweet

More Decks by Yoshiki Nakagawa

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. © 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^)/

    View Slide

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

    View Slide

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

    View Slide

  6. © 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
    }

    View Slide

  7. © 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())

    View Slide

  8. © 2022 LayerX Inc.
    8
    複数のパラメータがあり, 全てが必須な引数でない場合などに有効.
    ボディパラメータのオブジェクトに, キーなどが自由に設定できるAPI を想定.

    Functional Option
    {
    "email": "[email protected]",
    "custom_flag": true,
    "custom_key": "value"
    }

    View Slide

  9. © 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
    }
    }

    View Slide

  10. © 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

    View Slide

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

    View Slide

  12. © 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)

    View Slide

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

    View Slide

  14. © 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)

    View Slide

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

    View Slide

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

    View Slide

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

    }
    }
    func NewDefaultConfig() Config {
    return NewConfig(...)
    }

    View Slide

  18. © 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.
    アンチパターン

    View Slide

  19. © 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)

    View Slide

  20. © 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

    View Slide

  21. © 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)

    View Slide

  22. © 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)

    View Slide

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

    View Slide