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

More than 3 years have passed since last update.

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


GET


  • 例えばGETでHTTPリクエストを行う処理を実装したい場合、3通りの方法がある



    1. http.Get 関数を実行する


    2. Client 型の Get(url) メソッドを実行する


    3. Client 型の Do(request) メソッドを実行する



  • 単純なアクセスなら1で良い


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



  • url.Values でクエリを組み立てて、Get関数で指定したURLの末尾にEncode結果をパラメータとして付加すればOK



    • url.Valuesはクエリパラメータをkey-value形式で保持する型。ペアを追加するAddや上書きするSet等のメソッドが提供されている

    • 組み立てたクエリはEncodeメソッドを呼び出す事で?key1=value1&keyN=valueN形式に変換することが出来る



  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を生成する

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


Client.Do(Request) メソッド


  • Clientを生成した所で、上記2 or 3のいずれの方法でGETを投げるか?を考える。通常は 2 で良い

  • 一方 3 についてだが、引数で使用する Request 型について調べてみるとこんな特徴があった


    • Method種別をフィールドに保持している

    • HTTPプロトコルの情報をフィールドに保持している

    • formデータをフィールドに保持している

    • リクエストヘッダをフィールドに保持している = BASIC認証情報やUser-Agentを設定する事が出来る

    • アクセス先のURLを net.url.URL 型でフィールドに保持している


      • この型はscheme, 生のクエリ文字列, etcを内部に保持している





  • Request で保持しているこれらフィールドに内容を設定することで、より細かいアクセス制御が可能になる


Client.Do(Request)メソッドでGET実装を試す


  • まずクエリパラメータを組み立てる

  // クエリを組み立て

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


  • 次にRequest型のオブジェクトを http.NewRequest 関数で生成する

  // Request を生成

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


  • 組み立てたクエリをURLに付加する



    • Encode()メソッドを呼んでクエリ文字列を取得する

    • Requestで保持しているnet.url.URL型フィールドが内部にRawQueryという名前で生クエリを保持しているので、取得したクエリ文字列をこのフィールドに設定する



  // 組み立てたクエリを生クエリ文字列に変換して設定

req.URL.RawQuery = values.Encode()


  • リクエストヘッダを設定したい場合(例えばUser-Agentとか)は、Request内のHeaderフィールドに設定する


    • Values同様にSet()あるいはAdd()メソッドでkey-valueを設定すればOK



  // User-Agentを設定

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

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


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



    • client.Do(req)によるリクエスト結果は Response 型で返ってくる


      • レスポンスbodyはBodyフィールドに保持しており、この内容を使って後続処理を行う





  // 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


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


  • POSTなら 2 のhttp.PostForm(url, data)が一番楽


  • url.Valuesでクエリを組み立てて、PostForm関数の第2引数にそのまま渡せばOK

  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を使う

  // パラメータを組み立て

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


  • Request型のオブジェクトを http.NewRequest 関数で生成するが、 第3引数のbodyを先ほど組み立てたパラメータを使って設定する


    • エンコードしたパラメータを引数にして strings.NewReader 関数を実行し、取得した Reader を設定する



  // 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に設定する

  // Content-Typeを設定

req.Header.Add("Content-Type", "application/x-www-form-urlencoded")


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


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


  // 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トランザクションを実行し、与えられたリクエストに対するレスポンスを取得する



    • RoundTrip(*Request)というリクエストを実行する為のメソッドが定義されている


    • デフォルト実装としてDefaultTransportが提供されている



      • DefaultClientの内部で使われている

      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 - RountTripperインタフェースを実装している構造体型



    • Clientと比較してより低レベル層の設定を行う


      • proxyの設定


      • TLSの設定

      • keep-alives設定

      • データ圧縮の設定

      • etc...



    • HTTPリクエストは、実装済の RoundTrip(*Request) メソッド経由で投げる

    • Transportを介して確立した接続は、効率を考慮してキャッシュ・再利用されるようになっている


    • ほとんどのHTTPリクエスト実装は Clienthttp.Get, Post関数のように、Transportが内部にラップされている機能を使えば事足りる が、以下のようなケースでTransportを使う


      • file:/// のように http:// 以外のプロトコルでアクセスしたい場合



        • http.NewFileTransport(http.Dir("/")関数でfileプロトコル用のTransportを生成可能







  • Client, Transport共にスレッドセーフ(ゴルーチンセーフ)である