GETリクエスト
このページの例はどれも httptest.NewServer で独自のサーバーを開始し、localhostの本物のTCP接続を通して呼び出しています。srv.URL を https://api.example.com のような本物のアドレスに置き換えても、クライアントのコードは変わりません。
ここに表れている3つのルールは次のとおりです。
- まず
errをチェックする。 これは、そもそもレスポンスを得られなかった失敗を報告します。DNSのエラー、拒否された接続、タイムアウト、TLSの問題などです。 - エラーチェックの直後に
defer resp.Body.Close()とする。 ボディは接続を開いたまま保持しています。閉じられていないボディは接続をリークさせます。 - ボディを読むかデコードする。 テキストには
io.ReadAll、JSONにはjson.NewDecoder(resp.Body).Decode(&v)を使います。
ステータスコードはエラーではない
http.Get は、404 や 500 を含め、サーバーが送ったどんなレスポンスでも err == nil を返します。ステータスは自分でチェックしなければなりません。
io.LimitReader はエラーのボディを読む量に上限を設けるので、行儀の悪いサーバーのせいで数メガバイトをエラーメッセージに読み込むことはありません。多くのAPIは成功時に 201、202、204 を返します。どの2xxでもよいなら、resp.StatusCode >= 200 && resp.StatusCode < 300 をチェックします。
タイムアウト:本番のトラフィックにデフォルトのクライアントを使わない
http.Get、http.Post、http.DefaultClient には 全体のタイムアウトがありません(デフォルトのトランスポートが制限するのは接続とTLSハンドシェイクだけです)。接続を受け入れたまま応答しないサーバーは、ゴルーチンを永遠にブロックします。独自のクライアントを作りましょう。
Client.Timeout は、接続、リダイレクト、ボディの読み込みを含むやり取り全体を対象にします。返されるエラーは Timeout() == true を報告する *url.Error で、errors.As が net.Error インターフェースを通してそれを見つけます。クライアントは1つ作ってどこでも再利用します。http.Client は並行して使っても安全です。接続プールは Client ではなく Transport にあります。リクエストごとに新しい http.Client{} を作っても http.DefaultTransport とそのプールは共有されますが、リクエストごとに新しい http.Transport を作ると毎回空のプールから始まります。
コンテキストとヘッダー付きのリクエスト
http.NewRequestWithContext は、client.Do で送る前にカスタマイズできるリクエストを作ります。コンテキストはクライアントのタイムアウトに加えてリクエストごとの期限やキャンセルを与え、サーバーでは外向きの呼び出しを受け取ったリクエストの寿命に結びつけます。
ユーザーの入力をURLに連結する("?q=" + q)と、&、#、空白で壊れます。url.Values とその Encode メソッドがエスケープし、キーもソートしてくれます。パスの部品には url.PathEscape を使います。
JSONをPOSTする
client.Post(url, contentType, body) は、その Content-Type ヘッダーを持つリクエストの近道です。それ以外(認証ヘッダー、PUT、PATCH、DELETE)には、メソッドを指定した http.NewRequestWithContext と client.Do を使います。フォームのポストには、client.PostForm(url, url.Values{...}) が値をエンコードし、application/x-www-form-urlencoded を設定します。
接続の再利用
クライアントがTCP接続を再利用するのは、前のボディを最後まで読んで閉じたときだけです。途中で読むのをやめた場合(たとえばエラーのステータスの後)、同じホストに多くのリクエストを送るなら、閉じる前に空にします。
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
巨大になりうるボディでは、代わりに io.LimitReader を通して空にするか、接続を失うことを受け入れます。
リダイレクト
クライアントは最大10回までリダイレクトを自動でたどり、resp.Request.URL で最終的にたどり着いた場所がわかります。たとえば Location ヘッダーを読むために最初のリダイレクトで止めるには、CheckRedirect を設定します。
client := &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // return the 3xx response itself
},
}
よくある間違い
- 本番で
http.Getやhttp.DefaultClientを使う。 タイムアウトがありません。Timeout付きのhttp.Clientを作ります。 resp.Body.Close()を忘れる。 リクエストが失敗し始めるまで接続がリークします。errをチェックする前にボディを閉じる。errがnilでないときrespはnilで、resp.Body.Close()はpanicします。err == nilを成功とみなす。resp.StatusCodeをチェックします。- リクエストごとに新しい
http.Transportを作る。 トランスポートはそれぞれ独自の接続プールを持つので、すべてのリクエストが新しい接続(と新しいTLSハンドシェイク)を開きます。1つのトランスポートを、たいていは共有のhttp.Clientを通して共有します。 - クエリ文字列を手で組み立てる。
url.Valuesを使います。
よくある質問
GoでGETリクエストを送るには?
resp, err := http.Get(url) とし、err をチェックしてから defer resp.Body.Close() とし、io.ReadAll(resp.Body) でボディを読むか、json.NewDecoder(resp.Body).Decode(&v) でデコードします。resp.StatusCode もチェックしてください。404や500は http.Get のエラーにはなりません。
GoでHTTPリクエストにタイムアウトを設定するには?
タイムアウト付きのクライアント client := &http.Client{Timeout: 10 * time.Second} を作り、http.Get や http.DefaultClient の代わりに使います。これらには全体のタイムアウトがなく、接続を受け入れたまま応答しないサーバーは呼び出しを永遠にブロックします。リクエストごとの制限には、context.WithTimeout のコンテキストを使って http.NewRequestWithContext でリクエストを作ります。
GoでJSONのボディを持つPOSTリクエストを送るには?
値をマーシャルしてからポストします:body, _ := json.Marshal(v) と http.Post(url, "application/json", bytes.NewReader(body))。Authorization のようなヘッダーを加えるには、http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) で作り、req.Header.Set("Content-Type", "application/json") を設定し、client.Do(req) で送ります。
なぜGoではresp.Bodyを閉じる必要があるのですか?
ボディは開いた接続の上のストリームです。閉じると接続がプールに戻り、再利用されます。忘れると、プログラムが上限に達するまで接続とファイルディスクリプタがリークします。読まない場合でも閉じ、接続を再利用させたいなら最後まで読みます。