Go http.RoundTripper 実装ガイド

  • 32
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

こんにちわ、ワカルのアドベントカレンダー2日目を担当する包です。

最近はGoばかり書いているので、Goネタです。
外部への http アクセスをする時に構造を理解しておくと便利な、http.RoundTripper について書きます。

http.RoundTripper とは

Go で、外部にhttpアクセスするときには、 net/http パッケージにある、 http.Client を使います。
また、いろいろなAPIのクライアントライブラリの中でも殆どの場合 http.Client が使われていて、定義は以下のようになっています。(一部コメント省略)

type Client struct {
    // Transport specifies the mechanism by which individual
    // HTTP requests are made.
    // If nil, DefaultTransport is used.
    Transport RoundTripper

    CheckRedirect func(req *Request, via []*Request) error
    Jar CookieJar


    Timeout time.Duration
}

Transport というプロパティとして、http.RoundTripper を持っています。 nil のとき(デフォルト) には、 http.DefaultTransport を使います。
ローレベルのHTTP通信の実態は、こいつが担っていることがわかります。
http.RoundTripper の定義をみてみると

// RoundTripper is an interface representing the ability to execute a
// single HTTP transaction, obtaining the Response for a given Request.
//
// A RoundTripper must be safe for concurrent use by multiple
// goroutines.
type RoundTripper interface {
    // RoundTrip executes a single HTTP transaction, returning
    // the Response for the request req.  RoundTrip should not
    // attempt to interpret the response.  In particular,
    // RoundTrip must return err == nil if it obtained a response,
    // regardless of the response's HTTP status code.  A non-nil
    // err should be reserved for failure to obtain a response.
    // Similarly, RoundTrip should not attempt to handle
    // higher-level protocol details such as redirects,
    // authentication, or cookies.
    //
    // RoundTrip should not modify the request, except for
    // consuming and closing the Body, including on errors. The
    // request's URL and Header fields are guaranteed to be
    // initialized.
    RoundTrip(*Request) (*Response, error)
}

と、 RoundTrip という1つのメソッドを持つインターフェイスになっています。
コメントを要約すると、

  • RoundTripper は、複数のgorutineから呼ばれても大丈夫なようにしておく
  • レスポンスを解釈するべきではない
  • 高レベルの処理、認証やクッキーの操作などもすべきではない
    • (とはいえ、後述の oauth2.Transport など、認証機構をもつものもあるので、ケースバイケースだと思います)
  • レスポンスのステータスコードにかかわらず、レスポンスを取得すること自体の失敗のみに err を返しましょう
  • リクエストボディの読み込み、クローズ以外には、リクエストをさわらないようにしましょう

ということですかね。

巷の http.RoundTripper の例

http.fileTransport

標準パッケージに入っている、file:// プロトコルを扱うための実装

https://golang.org/pkg/net/http/#NewFileTransport

oauth2.Transport

oauth2 の認証周りを制御する実装。

https://www.godoc.org/golang.org/x/oauth2#Transport

実際のプロダクトでの使いどころ

現在、ワカルのサービス AIアナリスト では、以下の様な用途で独自の http.RoundTripper の実装を使っています。

  • 外部APIへのRateLimit制御 (秒間リクエスト数や、同時リクエスト数)
  • 外部API呼び出しに失敗した時の、リトライ処理 ( Exponential backoff 処理)
  • 一部レスポンスのキャッシュ

実装の基本形

一通り概観がつかめたら、自分の RoundTripper を実装をしてみましょう。何回か実装すると、ベースにするひな形が見えてきます。
以下はの例は、リクエストがされたかログするだけの実装です

package main

import (
    "log"
    "net/http"
)

type LogTransport struct {
    Transport http.RoundTripper
}

func (lt *LogTransport) transport() http.RoundTripper {
    if lt.Transport == nil {
        return http.DefaultTransport
    }
    return lt.Transport
}

func (lt *LogTransport) CancelRequest(req *http.Request) {
    type canceler interface {
        CancelRequest(*http.Request)
    }
    if cr, ok := lt.transport().(canceler); ok {
        cr.CancelRequest(req)
    }
}

func (lt *LogTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    res, err := lt.transport().RoundTrip(req)

    log.Printf("%d\t%s\t%s\n", res.StatusCode, req.Method, req.URL.String())

    return res, err
}

func main() {
    client := &http.Client{
        Transport: &LogTransport{},
    }
    if _, err := client.Get("http://goo.gl/hY1H85"); err != nil {
        log.Fatal(err)
    }
}

実行すると、以下の様なログがでると思います。

2015/12/02 00:38:00 301 GET http://goo.gl/hY1H85
2015/12/02 00:38:01 200 GET http://wacul.co.jp/

一回リダイレクトされて、 http://wacul.co.jp にアクセスしたようです。

抑えておきたいポイントは以下の2つかなと思います。

親の RoundTripper を設定できるようにする

実際のリクエスト処理を行う、親のRoundTripperを設定できるようにしておきましょう。多段に組み合わせて使うことができます。
また、設定されなかった場合には、http.DefaultTransport を使うようにしておきましょう。

CancelRequest を実装する

http.Client に Timeout を指定した場合などには、Transport の CancelRequest メソッドが実装されているかをチェックし、実装されていればが呼ばれることになっています。
自前でTransport を作る場合、親のTransportが、CancelRequest を実装していればそれを呼び出すように実装しておくと良いです。

https://golang.org/src/net/http/client.go?#L330

まとめ

http 関係をGoで実装するときには、http.RoundTripper について知っておいて損はないです。
大抵の Rest API クライアントや、 http 通信を行うライブラリは、外部から何らかの形で http.RoundTripper を指定できるようになっているので、応用がききます。

宣伝: ワカルでは、 Goを書きたいプログラマーを募集 しています!