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

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

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

Why not register and get more from Qiita?
  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
No 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
ユーザーは見つかりませんでした