Go http.RoundTripper 実装ガイド

こんにちわ、ワカルのアドベントカレンダー2日目を担当する包です。  
最近はGoばかり書いているので、Goネタです。
外部への http アクセスをする時に構造を理解しておくと便利な、`http.RoundTripper` について書きます。

## http.RoundTripper とは

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

```go
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` の定義をみてみると

```go
// 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](https://en.wikipedia.org/wiki/Exponential_backoff) 処理)
* 一部レスポンスのキャッシュ

## 実装の基本形

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

```go
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を書きたいプログラマーを募集](https://www.wantedly.com/projects/22162) しています！