😎

【Go】net/httpでGetしたjsonのResponseを見る

2022/02/20に公開

Railsエンジニアのタニシです!
再来月からGoを使ったプロジェクトにジョインするので、基本となるnet/httpを勉強しています。

何が書いてあるか

net/httpパッケージでGetした時の、レスポンスの中身

jsonをGetする

今回リクエスト先にはjsonplaceholder様を利用させて頂きました。
https://jsonplaceholder.typicode.com/

コードはこちら

main.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
  resp, err := http.Get("https://jsonplaceholder.typicode.com/posts")

  fmt.Printf("%+v\n", resp)
}

こちらの実行結果は以下です。

fish
❯ go run main.go
&{Status:200 OK StatusCode:200 Proto:HTTP/2.0 ProtoMajor:2 ProtoMinor:0 Header:map[Access-Control-Allow-Credentials:[true] Age:[24555] Alt-Svc:[h3=":443"; ma=86400, h3-29=":443"; ma=86400] Cache-Control:[max-age=43200] Cf-Cache-Status:[HIT] Cf-Ray:[6dbf1b91182c1f3b-NRT] Content-Type:[application/json; charset=utf-8] Date:[Fri, 11 Feb 2022 16:55:56 GMT] Etag:[W/"6b80-Ybsq/K6GwwqrYkAsFxqDXGC7DoM"] Expect-Ct:[max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"] Expires:[-1] Nel:[{"success_fraction":0,"report_to":"cf-nel","max_age":604800}] Pragma:[no-cache] Report-To:[{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=o05o0Kx%2B7Tu9KP3hZAEoAV5QWGiS%2FIeXnRHVC5ZHDvb1S3Pgk2hGMQaiqu5RGjTzakcwb3rEn%2BLUOML21HzYqQq103NLrXEjm0QTYwW7r8b%2FNRziD%2FQ1mcltL7nhvbkSFKTUu82GbauoQrLN5DtN%2F85xrmfqxfrPd2b8"}],"group":"cf-nel","max_age":604800}] Server:[cloudflare] Vary:[Origin, Accept-Encoding] Via:[1.1 vegur] X-Content-Type-Options:[nosniff] X-Powered-By:[Express] X-Ratelimit-Limit:[1000] X-Ratelimit-Remaining:[999] X-Ratelimit-Reset:[1644545002]] Body:0x1400007d1a0 ContentLength:-1 TransferEncoding:[] Close:false Uncompressed:true Trailer:map[] Request:0x14000144000 TLS:0x140003360b0}

さすがに見づらいので整形します

fish
&{
	Status:200 OK 
	StatusCode:200 
	Proto:HTTP/2.0
	ProtoMajor:2
	ProtoMinor:0
	Header:map[
		Access-Control-Allow-Credentials:[true]
		Age:[24555]
		Alt-Svc:[h3=":443"; ma=86400, h3-29=":443"; ma=86400]
		Cache-Control:[max-age=43200]
		Cf-Cache-Status:[HIT]
		Cf-Ray:[6dbf1b91182c1f3b-NRT]
		Content-Type:[application/json; charset=utf-8]
		Date:[Fri, 11 Feb 2022 16:55:56 GMT]
		Etag:[W/"6b80-Ybsq/K6GwwqrYkAsFxqDXGC7DoM"]
		Expect-Ct:[max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"]
		Expires:[-1]
		Nel:[{"success_fraction":0,"report_to":"cf-nel","max_age":604800}]
		Pragma:[no-cache]
		Report-To:[{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=o05o0Kx%2B7Tu9KP3hZAEoAV5QWGiS%2FIeXnRHVC5ZHDvb1S3Pgk2hGMQaiqu5RGjTzakcwb3rEn%2BLUOML21HzYqQq103NLrXEjm0QTYwW7r8b%2FNRziD%2FQ1mcltL7nhvbkSFKTUu82GbauoQrLN5DtN%2F85xrmfqxfrPd2b8"}],"group":"cf-nel","max_age":604800}]
		Server:[cloudflare]
		Vary:[Origin, Accept-Encoding]
		Via:[1.1 vegur]
		X-Content-Type-Options:[nosniff]
		X-Powered-By:[Express]
		X-Ratelimit-Limit:[1000]
		X-Ratelimit-Remaining:[999]
		X-Ratelimit-Reset:[1644545002]
	]
	Body:0x1400007d1a0
	ContentLength:-1
	TransferEncoding:[]
	Close:false
	Uncompressed:true
	Trailer:map[]
	Request:0x14000144000
	TLS:0x140003360b0
}

きちんとレスポンスが返ってきていますね。
それではこの中身を見ていきます。

構造体 Response

まずコードを実行して得られたものは何でしょう?
ポイントは1行目の&{です。
これはGoでは構造体を表します。(ちなみにRailsにもstructで定義する構造体があります)
なのでこれは構造体です。

ではどんな構造体でしょう?
型を見てみましょう。コードを少し変更します。

main.go
func main() {
	resp, _ := http.Get("https://jsonplaceholder.typicode.com/posts")

	fmt.Printf("%T\n", resp)
}

型を取れるようにしました。実行します。

fish
❯ go run main.go
*http.Response

httpパッケージのResponse型のようです。
型がわかったので公式ドキュメントを見てみましょう。

type Response
type Response struct {
	Status     string // e.g. "200 OK"
	StatusCode int    // e.g. 200
	Proto      string // e.g. "HTTP/1.0"
	ProtoMajor int    // e.g. 1
	ProtoMinor int    // e.g. 0

	// Header maps header keys to values. If the response had multiple
	// headers with the same key, they may be concatenated, with comma
	// delimiters.  (RFC 7230, section 3.2.2 requires that multiple headers
	// be semantically equivalent to a comma-delimited sequence.) When
	// Header values are duplicated by other fields in this struct (e.g.,
	// ContentLength, TransferEncoding, Trailer), the field values are
	// authoritative.
	//
	// Keys in the map are canonicalized (see CanonicalHeaderKey).
	Header Header

	// Body represents the response body.
	//
	// The response body is streamed on demand as the Body field
	// is read. If the network connection fails or the server
	// terminates the response, Body.Read calls return an error.
	//
	// The http Client and Transport guarantee that Body is always
	// non-nil, even on responses without a body or responses with
	// a zero-length body. It is the caller's responsibility to
	// close Body. The default HTTP client's Transport may not
	// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
	// not read to completion and closed.
	//
	// The Body is automatically dechunked if the server replied
	// with a "chunked" Transfer-Encoding.
	//
	// As of Go 1.12, the Body will also implement io.Writer
	// on a successful "101 Switching Protocols" response,
	// as used by WebSockets and HTTP/2's "h2c" mode.
	Body io.ReadCloser

	// ContentLength records the length of the associated content. The
	// value -1 indicates that the length is unknown. Unless Request.Method
	// is "HEAD", values >= 0 indicate that the given number of bytes may
	// be read from Body.
	ContentLength int64

	// Contains transfer encodings from outer-most to inner-most. Value is
	// nil, means that "identity" encoding is used.
	TransferEncoding []string

	// Close records whether the header directed that the connection be
	// closed after reading Body. The value is advice for clients: neither
	// ReadResponse nor Response.Write ever closes a connection.
	Close bool

	// Uncompressed reports whether the response was sent compressed but
	// was decompressed by the http package. When true, reading from
	// Body yields the uncompressed content instead of the compressed
	// content actually set from the server, ContentLength is set to -1,
	// and the "Content-Length" and "Content-Encoding" fields are deleted
	// from the responseHeader. To get the original response from
	// the server, set Transport.DisableCompression to true.
	Uncompressed bool

	// Trailer maps trailer keys to values in the same
	// format as Header.
	//
	// The Trailer initially contains only nil values, one for
	// each key specified in the server's "Trailer" header
	// value. Those values are not added to Header.
	//
	// Trailer must not be accessed concurrently with Read calls
	// on the Body.
	//
	// After Body.Read has returned io.EOF, Trailer will contain
	// any trailer values sent by the server.
	Trailer Header

	// Request is the request that was sent to obtain this Response.
	// Request's Body is nil (having already been consumed).
	// This is only populated for Client requests.
	Request *Request

	// TLS contains information about the TLS connection on which the
	// response was received. It is nil for unencrypted responses.
	// The pointer is shared between responses and should not be
	// modified.
	TLS *tls.ConnectionState
}

フィールドを見ると、確かにGetしたものと同じですね。
また、英語でいっぱいコメントつけてくれてますね。
TOEIC600点なのでDeepLで翻訳します。

type Response(翻訳)
type Response struct {
	Status     string // e.g. "200 OK"
	StatusCode int    // e.g. 200
	Proto      string // e.g. "HTTP/1.0"
	ProtoMajor int    // e.g. 1
	ProtoMinor int    // e.g. 0

        // Header は、ヘッダのキーと値を対応付ける。
        // レスポンスが同じキーを持つ複数のヘッダを持っていた場合、
        // それらはカンマ区切りで連結されるかもしれません。
        // (RFC7230のセクション3.2.2は、複数のヘッダーが意味的にカンマ区切りの
        // シーケンスと等価であることを要求している)。
        // Header の値がこの構造体の他のフィールド(例えば ContentLength, TransferEncoding, Trailer)
        // と重複している場合、そのフィールドの値を使用する。authoritative(権威的)である。
	//
	// マップ内のキーは正規化されます (CanonicalHeaderKey 参照)。
	Header Header

	// Body はレスポンスボディを表します.
	//
	// レスポンスボディはBodyフィールドが読み込まれる際にオンデマンドでストリーミングされる。
        // ネットワーク接続に失敗したり、サーバーが応答を終了した場合、
        // Body.Read の呼び出しはエラーを返します。
        //
        // httpクライアントとトランスポートは、ボディがないレスポンスや長さがゼロのレスポンス
        // であっても、ボディが常にゼロでないことを保証します。Bodyを閉じるのは呼び出し側の責任である。 
        // デフォルトのHTTPクライアントのTransportは、Bodyが最後まで読まれずに閉じられた場合、 
        // HTTP/1.xの「キープアライブ」TCPコネクションを再利用しないかもしれません。
        //
        // サーバーから返信があった場合、Bodyは自動的にデチャンクされます。
        // で、Transfer-Encodingを "chunked "にする。
        //
        // Go 1.12 では、Body は「101 Switching Protocols」レスポンスに成功すると io.Writer も実装されます。 
        // WebSocket や HTTP/2 の "h2c" モードで使用されるように。
	Body io.ReadCloser

        // ContentLength は、関連するコンテンツの長さを記録する。
        // 値-1は、長さが不明であることを示す。Request.Methodが "HEAD "でない限り,
        // 値 >= 0は,与えられたバイト数がBodyから読み取られるかもしれないことを示す。
	ContentLength int64

        // 最外周から最内周への転送エンコーディングが含まれる。
        // 値がnilの場合、"identity "エンコーディングが使用されることを意味する。
	TransferEncoding []string

        // Close は、Body を読んだ後、ヘッダが接続を閉じるように指示したかどうか
        // を記録する。 この値はクライアントへのアドバイスです。
        // ReadResponseもResponse.Writeも決してコネクションを閉じません。
	Close bool

        // Uncompressed は、レスポンスが圧縮されて送信されたが http パッケージによって
        // 伸長されたかどうかを報告します。Trueの場合、Bodyから読み込むと、
        // サーバーから実際に設定された圧縮コンテンツではなく、圧縮されていないコンテンツが得られ、 
        // ContentLengthは-1に設定され、responseHeaderから "Content-Length" と "Content-Encoding"
        // フィールドが削除されます。サーバーからオリジナルのレスポンスを取得するには、 
        // Transport.DisableCompressionをtrueに設定します。
	Uncompressed bool

        // Trailer は、Header と同じフォーマットでトレーラーのキーと値を対応付ける。
        //
        // Trailerは、最初はnil値のみを含み、サーバーの "Trailer "ヘッダー値で指定された各キーに対して
        // 1つずつ含まれる。これらの値はHeaderに追加されない。
        //
        // Trailerは、BodyのReadコールと同時にアクセスしてはならない。
        //
        // Body.Readがio.EOFを返した後、Trailerにはサーバから送られたトレイラ値が格納される
	Trailer Header

        // Request は、この Response を取得するために送信されたリクエストである。
        // RequestのBodyはnilである(すでに消費されている)。
        // これはClientのリクエストに対してのみ入力される。
	Request *Request

        // TLS は、応答を受信した TLS 接続に関する情報を含む。
        // 暗号化されていないレスポンスでは、これはnilである。
        // このポインターはレスポンス間で共有され、変更されるべきではありません。
	TLS *tls.ConnectionState
}

わかりやすくなりましたね。
この構造体を把握すれば、いろいろわかりそうです。

構造体 Reponseのソースコードを読む

上から見ていきましょう。

Status, StatuCode

レスポンスのステータスコードのようです。
StatusとStatusCodeという2つのフィールドで提供されるようです。
以下のような違いがありますで、Statusの方はいつ使うんだ?

field type e.g.
Status string "200 OK"
StatusCode int 200

Proto, ProtoMajor, ProtoMinor

ソースコードの説明をみるとこうあります。

// The protocol version for incoming server requests.
...
Proto      string // "HTTP/1.0"
ProtoMajor int    // 1
ProtoMinor int    // 0

つまりHTTPプロトコルのバージョンを表すようです。

field type e.g. 備考
Proto string "HTTP/1.0" バージョン
ProtoMajor int 1 メジャーバージョン
ProtoMinor int 0 マイナーバージョン

そのままですね

Body

こちらもそのままですが、ソースコードに気になる文言があります。

// Bodyを閉じるのは呼び出し側の責任である。

実は、実際にBodyの中身を利用する際には次のようなコードを書きます。

main.go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

type Post struct {
	UserId int    `json:"userId"`
	Id     int    `json:"id"`
	Title  string `json:"title"`
	Body   string `json:"body"`
}

func main() {
	var posts []Post

	resp, err := http.Get("https://jsonplaceholder.typicode.com/posts")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		fmt.Println("Error: status code", resp.StatusCode)
		return
	}

	body, _ := io.ReadAll(resp.Body)

	if err := json.Unmarshal(body, &posts); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("%+v\n", posts)
}

この中のdefer resp.Body.Close()に注目してください。
deferはmain関数の最後に実行するということです。
つまり、Bodyの内容をごにょごにょした最後にBodyを閉じるということです。

なぜCloseする必要があるのかは、こちらの記事で詳しく解説してくれています。
closeしないと「TCPコネクションがクローズされない」としないと書いてるのは怖いですね。
https://qiita.com/stk0724/items/dc400dccd29a4b3d6471

ContentLength

そのまま。
一般的なHTTPヘッダにもContent-Lengthがあります。

TransferEncoding, Uncompressed

こちらの2つはセット感があったのでまとめて取り扱います。

Uncompressed

サーバー => クライアントに転送されるデータが圧縮されて送られてきたかどうか

TransferEncoding

データ転送に使用した形式
Goではどんな選択肢があるかわかりませんが、一般的には以下の選択肢があるようです。

形式 説明
chunked チャンク (塊) の連続で送られる
compress Lempel-Ziv-Welch (LZW) アルゴリズムを使用した形式. 今は使わない
deflate zlib 構造体と deflate 圧縮アルゴリズムを使用
gzip gzipを使用
identity 圧縮なし (Goではデフォルト)

Close

ソースコードにもあるように、Bodyを読み込んだ後にちゃんとCloseしたかどうかを保存しておくところ?
クライアントへのアドバイスとしてのフィールドらいしので、Closeしないまま終わろうとしたら例外吐くようになってるのかな?

Trailer

一般的なhttpのTrailer

Trailer応答ヘッダは、メッセージの完全性チェック、デジタル署名、後処理の状態など、メッセージ本体の送信中に動的に生成される可能性のあるメタデータを提供するために、送信者がチャンクされたメッセージの末尾に追加フィールドを含めることを可能にします。

Request

リクエストに使用した情報
通常では空らしい?

TLS

通信を暗号化している場合はそれに関する情報が記載

まとめ

Railsエンジニアの私としては、フレームワークを使用しないアプリケーションなど考えられないのだけれど、Goではフレームワークを使用せず、標準パッケージのnet/httpでWebサーバーを構築することも普通にあるようです。
net/httpもっと深く理解する必要がありますね。

Discussion