Go net/httpパッケージの概要とHTTPクライアント実装例

Goは標準パッケージが充実しているのが特徴の1つだが、`net/http`も例外ではなくHTTPクライアントの処理が簡単に書ける

## GET
* 例えばGETでHTTPリクエストを行う処理を実装したい場合、3通りの方法がある
    1. [http.Get](http://golang.org/pkg/net/http/#Get) 関数を実行する
    1. [Client](http://golang.org/pkg/net/http/#Client) 型の [Get(url)](http://golang.org/pkg/net/http/#Client.Get) メソッドを実行する
    1. [Client](http://golang.org/pkg/net/http/#Client) 型の [Do(request)](http://golang.org/pkg/net/http/#Client.Do) メソッドを実行する
* 単純なアクセスなら1で良い

### 一番単純な http.Get(url)によるGET

* [url.Values](http://golang.org/pkg/net/url/#Values) でクエリを組み立てて、`Get`関数で指定したURLの末尾に`Encode`結果をパラメータとして付加すればOK
    * `url.Values`はクエリパラメータをkey-value形式で保持する型。ペアを追加する`Add`や上書きする`Set`等のメソッドが提供されている 
    * 組み立てたクエリは`Encode`メソッドを呼び出す事で`?key1=value1&keyN=valueN`形式に変換することが出来る

```go
  values := url.Values{}
  values.Add([クエリのキー], [値]

  resp, err := http.Get([アクセス先URL] + "?" + values.Encode())
  if err != nil {
    fmt.Println(err)
    return
  }

  // 関数を抜ける際に必ずresponseをcloseするようにdeferでcloseを呼ぶ
  defer resp.Body.Close()

  // Responseの内容を使用して後続処理を行う
  execute(resp)
```

### Client 型
* 2, 3のようにClient型のメソッド経由でのアクセスは、接続時の細かい設定を行いたい場合に用いる
    * Cookieを使いたい - Clientの`Jar`フィールドをいじる
    * タイムアウトを設定したい - Clientの`Timeout`フィールドをいじる
    * リクエスト時の各種属性値を設定したい - `Request`型を引数に取る`Do(request)`メソッドを使う
* デフォルト実装として`DefaultClient`が用意されている
    * 内部では`var DefaultClient = &Client{}`と定義されている。要は空(ゼロ値)のClient 
    * `http.Get(url)`関数は`DefaultClient.Get(url)`のラッパー 
* HTTPリクエストを処理する際は、後述する`RoundTripper`インタフェースのデフォルト実装である`Transport`が内部で使われている
    * DefaultClientは、Transportのデフォルト実装`DefaultTransport`を内部で使っている
* 例えば「タイムアウトを10秒に設定したい」という場合は↓こんな感じのClientを生成する

```go
  client := &http.Client{Timeout: time.Duration(10) * time.Second}
```

### Client.Do(Request) メソッド
* Clientを生成した所で、上記2 or 3のいずれの方法でGETを投げるか？を考える。通常は 2 で良い
* 一方 3 についてだが、引数で使用する [Request](http://golang.org/pkg/net/http/#Request) 型について調べてみるとこんな特徴があった
    * Method種別をフィールドに保持している
    * HTTPプロトコルの情報をフィールドに保持している
    * formデータをフィールドに保持している
    * リクエストヘッダをフィールドに保持している = BASIC認証情報やUser-Agentを設定する事が出来る
    * アクセス先のURLを [net.url.URL](http://golang.org/pkg/net/url/#URL) 型でフィールドに保持している
        * この型はscheme, 生のクエリ文字列, etcを内部に保持している
* Request で保持しているこれらフィールドに内容を設定することで、より細かいアクセス制御が可能になる


### Client.Do(Request)メソッドでGET実装を試す
* まずクエリパラメータを組み立てる

```go
  // クエリを組み立て
  values := url.Values{} // url.Valuesオブジェクト生成
  values.Add([クエリのキー], [値]) // key-valueを追加
```

* 次にRequest型のオブジェクトを [http.NewRequest](http://golang.org/pkg/net/http/#NewRequest) 関数で生成する

```go
  // Request を生成
  req, err := http.NewRequest("GET", [アクセス先URL], nil)
  if err != nil {
    fmt.Println(err)
    return
  }
```

* 組み立てたクエリをURLに付加する
    * `Encode()`メソッドを呼んでクエリ文字列を取得する
    * Requestで保持している`net.url.URL`型フィールドが内部に`RawQuery`という名前で生クエリを保持しているので、取得したクエリ文字列をこのフィールドに設定する

```go
  // 組み立てたクエリを生クエリ文字列に変換して設定
  req.URL.RawQuery = values.Encode()
```

* リクエストヘッダを設定したい場合（例えばUser-Agentとか）は、Request内の`Header`フィールドに設定する
    * Values同様に`Set()`あるいは`Add()`メソッドでkey-valueを設定すればOK

```go
  // User-Agentを設定
  req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MAFSJS; rv:11.0) like Gecko")
```

* (おまけ) BASIC認証が必要なら [Request.SetBasicAuth](http://golang.org/pkg/net/http/#Request.SetBasicAuth) メソッドで設定する

```go
  req.SetBasicAuth([USER], [PASS})
```

* 最後に`Do(req)`メソッドで実際にリクエストを投げる
    * `client.Do(req)`によるリクエスト結果は [Response](http://golang.org/pkg/net/http/#Response) 型で返ってくる
        * レスポンスbodyは`Body`フィールドに保持しており、この内容を使って後続処理を行う

```go
  // Doメソッドでリクエストを投げる
  // http.Response型のポインタ（とerror）が返ってくる
  resp, err := client.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }

  // 関数を抜ける際に必ずresponseをcloseするようにdeferでcloseを呼ぶ
  defer resp.Body.Close()

  // Responseの内容を使用して後続処理を行う
  execute(resp)


func execute(resp *http.Response) {
  // response bodyを文字列で取得するサンプル
  // ioutil.ReadAllを使う
  b, err := ioutil.ReadAll(resp.Body)
  if err == nil {
    fmt.Println(string(b))
  }
}
```


## POST
* POSTの場合も、5通りの方法がある
    1. [http.Post](http://golang.org/pkg/net/http/#Post) 関数を実行する
    1. [http.PostForm(url, data)](http://golang.org/pkg/net/http/#PostForm) 関数を実行する
    1. [Client](http://golang.org/pkg/net/http/#Client) 型の [Post(url, bodyType, body)](http://golang.org/pkg/net/http/#Client.Post) メソッドを実行する
    1. [Client](http://golang.org/pkg/net/http/#Client) 型の [PostForm(url, data)](http://golang.org/pkg/net/http/#Client.PostForm) メソッドを実行する
    1. [Client](http://golang.org/pkg/net/http/#Client) 型の [Do(request)](http://golang.org/pkg/net/http/#Client.Do) メソッドを実行する
* 使い分けの指針についてはGETと同様

### 一番単純な http.PostForm(url, data)によるPOST

* POSTなら 2 の`http.PostForm(url, data)`が一番楽
* `url.Values`でクエリを組み立てて、`PostForm`関数の第2引数にそのまま渡せばOK

```go
  values := url.Values{}
  values.Add([クエリのキー], [値]

  resp, err := http.PostForm([アクセス先URL], values)
  if err != nil {
    fmt.Println(err)
    return
  }

  :
  :
```

### Client.Do(Request)メソッドでPOST実装を試す
* GETと同じく、最も細かく設定を制御することが出来る`Client.Do(Request)`メソッドを使ったPOSTアクセスを実装してみる
* まずパラメータ(Formの)を組み立てる　※GETと同じく`url.Values`を使う

```go
  // パラメータを組み立て
  values := url.Values{} // url.Valuesオブジェクト生成
  values.Add([クエリのキー], [値]) // key-valueを追加
```

* Request型のオブジェクトを [http.NewRequest](http://golang.org/pkg/net/http/#NewRequest) 関数で生成するが、 __第3引数のbodyを先ほど組み立てたパラメータを使って設定する__
    * エンコードしたパラメータを引数にして [strings.NewReader](http://golang.org/pkg/strings/#NewReader) 関数を実行し、取得した `Reader` を設定する

```go
  // Request を生成
  req, err := http.NewRequest("POST", [アクセス先URL], strings.NewReader(values.Encode()))
  if err != nil {
    fmt.Println(err)
    return
  }
```

* `Content-Type`リクエストヘッダを`application/x-www-form-urlencoded`に設定する

```go
  // Content-Typeを設定
  req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
```

* 他のリクエストヘッダやBASIC認証を使いたければ適宜`req.Header.Add`して追加する

* 最後に`Do(req)`メソッドで実際にリクエストを投げる

```go
  // Doメソッドでリクエストを投げる
  // http.Response型のポインタ（とerror）が返ってくる
  resp, err := client.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }

  // 関数を抜ける際に必ずresponseをcloseするようにdeferでcloseを呼ぶ
  defer resp.Body.Close()

  // Responseの内容を使用して後続処理を行う
  execute(resp)
```

## 作ったスニペット
https://gist.github.com/goldeneggg/bfb6ecf8cfc017402c24

## ※補足

### RoundTripperとTransport
* [http.RoundTripper](http://golang.org/pkg/net/http/#RoundTripper) インタフェース - 単一のHTTPトランザクションを実行し、与えられたリクエストに対するレスポンスを取得する
    * `RoundTrip(*Request)`というリクエストを実行する為のメソッドが定義されている
    * デフォルト実装として`DefaultTransport`が提供されている
        * `DefaultClient`の内部で使われている
        
        ```go
        var DefaultTransport RoundTripper = &Transport{
          Proxy: ProxyFromEnvironment,  // proxyの設定は環境変数 $HTTP_PROXY, $NO_PROXY の内容を引き継ぐ
          Dial: (&net.Dialer{
                 Timeout:   30 * time.Second,  // 接続タイムアウト時間
                 KeepAlive: 30 * time.Second,  // 1TCP接続あたりの持続時間=keeyAlive
              }).Dial,
          TLSHandshakeTimeout: 10 * time.Second,
        }
        ```
 
* [http.Transport](http://golang.org/pkg/net/http/#Transport) - RountTripperインタフェースを実装している構造体型
    * `Client`と比較してより低レベル層の設定を行う
        * proxyの設定   
        * TLSの設定
        * keep-alives設定
        * データ圧縮の設定
        * etc...
    * HTTPリクエストは、実装済の [RoundTrip(*Request)](http://golang.org/pkg/net/http/#Transport.RoundTrip) メソッド経由で投げる
    * Transportを介して確立した接続は、効率を考慮してキャッシュ・再利用されるようになっている
    * __ほとんどのHTTPリクエスト実装は `Client`や`http.Get, Post`関数のように、Transportが内部にラップされている機能を使えば事足りる__ が、以下のようなケースでTransportを使う
        * file:/// のように http:// 以外のプロトコルでアクセスしたい場合
            * `http.NewFileTransport(http.Dir("/")`関数でfileプロトコル用のTransportを生成可能
* __Client, Transport共にスレッドセーフ（ゴルーチンセーフ）である__