LoginSignup
310
260

More than 5 years have passed since last update.

Goでnet/httpを使う時のこまごまとした注意

Last updated at Posted at 2014-12-21

httpクライアント編

レスポンスを受け取ったら必ずBodyをCloseすること

resp, err := http.DefaultClient.Do(req)
if err != nil {
  return err
}
defer resp.Body.Close()

HTTPレスポンスを受け取ったとき、err != nilのときresp.Bodyは常に非nilである(たとえBodyが0バイトであっても)。このresp.BodyClose するのは呼び出し側の責務である。Body.Close を怠ると、Keep-Alive(デフォルトで有効)のためにTCPコネクションが再利用されない。…ということが ドキュメントに口を酸っぱくして書いてある。

同一ホストへのコネクション数はデフォルトで最大2に制限されている

同一ホストへのコネクション数はhttp.DefaultMaxIdleConnsPerHost定数によりデフォルトで2に制限されている。

// DefaultMaxIdleConnsPerHost is the default value of Transport's
// MaxIdleConnsPerHost.
const DefaultMaxIdleConnsPerHost = 2

ベンチマーク目的などで同一ホストへの最大コネクション数を変更する場合は、http.ClientのTransportのMaxIdleConnsPerHostメンバを0以外に設定する。(0のときはDefaultMaxIdleConnsPerHostになる)

以下のように設定する。

// DefaultTransportの制限を変更する場合。
// DefaultTransportはhttp.DefaultClientのTransportとして使われる。
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 32

// 自分で用意したhttp.Clientの制限を変更する場合。
c := http.Client{
  Transport: http.Transport{ MaxIdleConnsPerHost: 32 },
  }
res, err := c.Get("http://example.com")

http.Clientはグローバル変数を使い回そう

@methane さんの指摘で追加しました。ありがとうございます)

The Client's Transport typically has internal state (cached TCP connections), so Clients should be reused instead of created as needed. Clients are safe for concurrent use by multiple goroutines.

ドキュメントにある通り、http.Clientは内部のTransportメンバとしてTCPコネクションの管理機構(RoundTripper)を持っているので、出来る限り使い回すべきです。http.Clientは複数Goroutineからの並行利用に対して安全です。

http.Clientは、常にグローバルで確保されているhttp.DefaultClientを使えばいいと思います(http.Get()などの関数も内部的にDefaultClient`を利用しています)。

@methane さんのコメントもご参照ください。(ゼロ値で初期化したhttp.Clientはhttp.DefaultTransportを参照するので問題ないはずですが、特に理由がなければ使わない方がいいでしょう)

httpサーバ編

WriteHeaderの前にContent-Typeを付与すること

Writeした時点でContent-Typeが空だと、httpサーバーは自動的にMIME-TYPE判定のためにレスポンスボディの先頭512bytesをバッファしたりする。それを避けるためにContent-Typeをつけておくこと。

w.Header().Set("Content-Type","text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, world"))

url.Query()を何度も呼ばないこと

サーバがリクエストのクエリ文字列を得る時にurl.Query()は便利だが、これは構築済みのurl.Valuesを返している訳ではなく、コールごとに生URLからパース処理をしてから戻り値を返しているので、何度も呼ぶのは非効率。
代わりに、r.ParseForm()でURLのクエリパラメータとBodyのPUTまたはPOSTのformパラメータがパースされてRequest.ParseFormに格納されるのでこれを使う。formパラメータとの区別が必要ないのであればこの値を使おう。(なぜformパラメータと混ざっているのだろう…)

追記

Formパラメータと分離しておきたい場合は普通に q := url.Query() と変数に代入して使おう。

RequestのContent-Lengthはr.ContentLengthから取ろう

r.Header().Get("Content-Length")は文字列なのでパースや異常値の扱い面倒。
http.RequestContentLength int64が用意されており、こちらを使った方が簡単。
Content-Lengthヘッダが存在しなかったり、不正な値だったりした場合はr.ContentLengthに-1が代入され、ヘッダは削除される。
(Response構造体にも同様にContentLengthメンバが用意されているので、クライアントでも同様である)

Content-Lengthヘッダを設定しないレスポンスは、長いと自動的にchunk転送になる

デフォルトで2048バイト(http.bufferBeforeChunkingSize)までのレスポンスであれば、自動的にContent-Lengthヘッダが設定される。
それを越えると、レスポンスは自動的にchunk転送に切り替わり、ヘッダにはTransfer-Encoding:chunkがつく。
チャンク転送になると困る場合は、事前にContent-Lengthヘッダを設定しておこう。

310
260
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
310
260