Closed5

Go で HTTP リクエストを送りたい

yukiyuki

http パッケージにある NewRequest という関数を利用すれば送信可能みたい。

// NewRequest wraps NewRequestWithContext using the background context.
func NewRequest(method, url string, body io.Reader) (*Request, error) {
	return NewRequestWithContext(context.Background(), method, url, body)
}

一応、NewRequestWithContext も気になるので読んでみる。

// NewRequestWithContext returns a new Request given a method, URL, and
// optional body.
//
// If the provided body is also an io.Closer, the returned
// Request.Body is set to body and will be closed by the Client
// methods Do, Post, and PostForm, and Transport.RoundTrip.
//
// NewRequestWithContext returns a Request suitable for use with
// Client.Do or Transport.RoundTrip. To create a request for use with
// testing a Server Handler, either use the NewRequest function in the
// net/http/httptest package, use ReadRequest, or manually update the
// Request fields. For an outgoing client request, the context
// controls the entire lifetime of a request and its response:
// obtaining a connection, sending the request, and reading the
// response headers and body. See the Request type's documentation for
// the difference between inbound and outbound request fields.
//
// If body is of type *bytes.Buffer, *bytes.Reader, or
// *strings.Reader, the returned request's ContentLength is set to its
// exact value (instead of -1), GetBody is populated (so 307 and 308
// redirects can replay the body), and Body is set to NoBody if the
// ContentLength is 0.
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
// (以下略)
  • 基本的に普段 JSON を渡す際と同じで、io.Reader を使用すればよい。
  • context.Context はリクエストのライフタイムを扱う。たとえば、コネクションの取得、リクエストの送信、それとレスポンスヘッダとレスポンスボディの読み込み。
  • ContentLength が0だった場合には、NoBody というものがセットされるらしい。NoBody は下記に示すように、要するに空の構造体。Read をしたとしても EOF が返るなどの特別な実装がなされている。
// NoBody is an io.ReadCloser with no bytes. Read always returns EOF
// and Close always returns nil. It can be used in an outgoing client
// request to explicitly signal that a request has zero bytes.
// An alternative, however, is to simply set Request.Body to nil.
var NoBody = noBody{}

type noBody struct{}

io.Reader は interface だから、下記のような具体的な型が使用できるみたい。

		switch v := body.(type) {
		case *bytes.Buffer:
			req.ContentLength = int64(v.Len())
			buf := v.Bytes()
			req.GetBody = func() (io.ReadCloser, error) {
				r := bytes.NewReader(buf)
				return ioutil.NopCloser(r), nil
			}
		case *bytes.Reader:
			req.ContentLength = int64(v.Len())
			snapshot := *v
			req.GetBody = func() (io.ReadCloser, error) {
				r := snapshot
				return ioutil.NopCloser(&r), nil
			}
		case *strings.Reader:
			req.ContentLength = int64(v.Len())
			snapshot := *v
			req.GetBody = func() (io.ReadCloser, error) {
				r := snapshot
				return ioutil.NopCloser(&r), nil
			}
yukiyuki

今回は URL を保持する構造体を JSON に変えてさらに別のサーバーに POST で投げたいというシンプルなものを実装しようとしてる。

Go の場合、次のような手順をもとに実装する。

  1. 作りたい対象の構造体を生成しておく。
  2. json.Marshal して JSON → []byte に変換しておく。
  3. http.NewRequest の body に2で作成したバイト配列を入れてリクエストを送る。

適当な構造体を用意する。

type UrlList struct {
	Urls []string `json:"urls"`
}

作る。

urls := UrlList{Urls:[]string{"http://www.google.com"}}

マーシャルする

urlJson, err := json.Marshal(urls)
if err != nil {
	return err
}

入れてリクエストを投げる。

req, err := http.NewRequest(http.MethodPost, "localhost:5000/photos", bytes.NewBuffer(urlJson))

Marshal, Unmarshal

実は Hazelcast を使っていたときにこの言葉をよく見ていたので、おおよそイメージはなんとなくつく。Serialize/Deserialize という言葉を言語によっては使っていたりするが、それとほぼ同じことをしている。

Marshal というのは隊列を組む("整列させる"くらいの意味だと思う)ときに使う言葉で、反対語は隊列を崩すから Unmarshal となる。

各値がどのような形でマーシャルされるかは、この json.Marshal 関数にかなり詳しく書いてあるので、それを参照するとよさそう。

中の実装も読んでみた。

実装としては単純で、リフレクションをかけて再帰的に値の変換をかけているという感じがした(あまり深くは追ってない)。

yukiyuki

実際に動かしてみたところ、ちょっといろいろ足りていなくて調査したので続き。

localhost:5000/photos にリクエストを投げたい場合、http:// を頭につける必要がある(内部的にプロトコルを補完してくれる処理はついていないみたい)。

また、リクエストを投げる際には http.Client を利用する必要がある。これについている Do 関数を用いて、最終的にはリクエストを送信できる。

というわけで、最終的な送信コードは下記のようになった。30秒間のタイムアウトの設定も込み。

	req, err := http.NewRequest(http.MethodPost, "http://localhost:5000/photos", bytes.NewBuffer(urlJson))
	if err != nil {
		return err
	}

	client := &http.Client{
		Timeout:       30 * time.Second,
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}

	println(resp.StatusCode)
yukiyuki

ちなみに、http.Client のドキュメントに案内があるようにとくに何も設定する必要がない場合には http.DefaultClient が使用できる。

http.DefaultClienthttp.Client の構造体のゼロ値になっている。簡単な動作確認の際に使用できそうでいいと思った。

// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}

ドキュメントにもあるとおり、http.Post の実装などは DefaultClient が呼び出されている(逆に言うと自分でカスタマイズしてもこれが呼ばれるので注意が必要そう?)。

// Post issues a POST to the specified URL.
//
// Caller should close resp.Body when done reading from it.
//
// If the provided body is an io.Closer, it is closed after the
// request.
//
// Post is a wrapper around DefaultClient.Post.
//
// To set custom headers, use NewRequest and DefaultClient.Do.
//
// See the Client.Do method documentation for details on how redirects
// are handled.
func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	return DefaultClient.Post(url, contentType, body)
}
yukiyuki

さて、Client を呼んでいたら RoundTripper というものが気になった。少し読んでみると、

// RoundTripper is an interface representing the ability to execute a
// single HTTP transaction, obtaining the Response for a given Request.

リクエスト〜レスポンスまでの1つの HTTP のトランザクションを表現するインタフェースらしい。

今回は時間の都合上あまり深く追わないけど、いくつか参考になりそうなブログも発見した。

機会があったら実装してみたい。

このスクラップは2020/11/25にクローズされました