1. Qiita
  2. 投稿
  3. Go

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

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

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共にスレッドセーフ(ゴルーチンセーフ)である

Comments Loading...