S.Hayakawaをフォローする

GoのHTTPクライアントについて

バックエンド

HTTPリクエストを受け取るサーバーの作り方を覚えたら、
次はHTTPリクエストを送信するクライアントについて知りたくなりますよね!?

外部サービスのAPIをコールする時や、マイクロサービス内のAPIを呼び出す時など
クライアントを使用する機会は多くあると思います。
ということでGoのHTTPクライアントについて社内勉強会で紹介していきます。

GoのHTTPクライアント

標準ライブラリのnet/httpパッケージを使ってHTTPリクエストを送信します。
https://pkg.go.dev/net/http

リクエスト送信の一番簡単な方法としては、http.Get(), http.Post()を使うことです。
これらはリクエスト時の細かい制御がしにくいので、
詳細な設定を行いたい場合は、http.Clientを生成してカスタムする方法をとります。

一例として、次のようなHTTPクライアントのカスタマイズが考えられます。
・認証情報を付与する
・タイムアウトの設定
・リトライ制御
・リクエストのロギング

具体的な使い方を見ていきます。

環境

Go 1.22.3

http.Get(), http.Post()

http.Get()
func main() {
	res, err := http.Get("http://example.com")
	if err != nil {
		//
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		//
	}
}

ポイント
res.StatusCodeでステータスコードを確認する
res.Body.Close()でレスポンスボディをCloseする

http.Get()内でtcpコネクションが生成されます。
これをCloseしないでいると、リソースを圧迫してしまいます。

http.Post()
func main() {
	body := []byte(`{"id": 1223, "name": "gopher"}`)
	buf := bytes.NewBuffer(body)

	res, err := http.Post("http://example.com/post", "application/json", buf)
	if err != nil {
		//
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		//
	}
}

ポイント
http.Post()の第2引数でcontent-typeを指定

http.Client

http.Clientを使用してタイムアウトの設定や認証情報の付与などを行えます。

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

	req, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
	if err != nil {
		//
	}

	req.Header.Add("Authorization", "Bearer token")

	res, err := client.Do(req)
	if err != nil {
		//
	}
	defer res.Body.Close()
}

ポイント:
http.Client{}を使用してHTTPクライアントをカスタマイズできる
http.NewRequest ( http.NewRequestWithContext() ) でリクエストを生成し、ヘッダー情報を付与するなど

ここまでは概ねシンプルにリクエストを送り、レスポンスを受け取るだけの機能です。
プロジェクトの要件により、外部サービスとの通信をもっと細かく制御したいことがあると思います。
具体的にはリトライを仕込んだり、レートリミットを設けたりなどです。
以下ではその方法を説明していきます。

http.RoundTripper

http.ClientにはRoundTripperインターフェースのTransportフィールドがあります。
https://pkg.go.dev/net/http#Client

RoundTripperはhttp.Clientの中でリクエストを受け取り、レスポンスを返す部分を司っています。
https://pkg.go.dev/net/http#RoundTripper

このRoundTripperインターフェースをカスタマイズしてTransportにセットすることで、
リクエスト前やレスポンス後に行いたい共通処理を持たせたクライアントを生成することができます。

type myRoundTripper struct {
	t http.RoundTripper
}

func (r myRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	// リクエスト前の処理
	// code ..

	res, err := r.t.RoundTrip(req)

	// レスポンス後の処理
	// code ..
	return res, err
}

func main() {
	client := &http.Client{
		// TransportにカスタムしたRoundTripperをセット
		Transport: &myRoundTripper{
			t: http.DefaultTransport,
		},
	}

	req, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
	if err != nil {
		// 
	}

	res, err := client.Do(req)
	if err != nil {
		// 
	}
	defer res.Body.Close()
}

全体の骨格はこのような形です。
カスタムRoundTripperをセットしたClientでリクエストを行うことで、
リクエスト時の細かい制御を埋め込むことができます。

実際にClientにリトライ制御を仕込んでみましょう。

var retryStatus = map[int]struct{}{
	http.StatusInternalServerError: struct{}{},
	http.StatusTooManyRequests:     struct{}{}
}

type myRoundTripper struct {
	t        http.RoundTripper
	maxRetry int
	wait     time.Duration
}

func (r myRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {

	var res *http.Response
	var err error

	for c := 0; c < r.maxRetry; c++ {
		res, err = r.t.RoundTrip(req)

		// リトライすべきステータスか確認する
		if _, ok := retryStatus[res.StatusCode]; !ok {
			return res, err
		}

		select {
		case <-req.Context().Done(): // contextの期限が来たら終了
			return nil, req.Context().Err()
		case <-time.After(r.wait): // リトライ間隔を待つ
		}
	}

	return res, err
}

func main() {
	client := &http.Client{
		Transport: &myRoundTripper{
			t:        http.DefaultTransport,
			maxRetry: 3,
			wait:     2 * time.Second,
		},
	}

	req, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
	if err != nil {
		// 
	}

	res, err := client.Do(req)
	if err != nil {
		// 
	}
	defer res.Body.Close()
}

ポイント:
・不正なリクエストパラメータのエラー(400)など、リトライしても結果が変わらないことが明白な場合はリトライしない
・今回の例では、クライアント生成時にリトライ回数とリトライ間隔を設定できるようにしている

この他にも、ログの追加やレートリミットの制御など様々な実装が可能です。
こちらで実装例が紹介されていますので、併せてご参考ください!
http.RoundTripperでHTTPクライアントを拡張する

参考

書籍

  • 実用Go言語 . O’Reilly Japan ( 渋川 よしき、辻 大志郎、真野 隼記 著 )

Web