Help us understand the problem. What is going on with this article?

Go http.RoundTripper 実装ガイド

More than 3 years have passed since last update.

こんにちわ、ワカルのアドベントカレンダー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を書きたいプログラマーを募集 しています!

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away