【Go】net/httpでGetしたjsonのResponseを見る
Railsエンジニアのタニシです!
再来月からGoを使ったプロジェクトにジョインするので、基本となるnet/httpを勉強しています。
何が書いてあるか
net/httpパッケージでGetした時の、レスポンスの中身
jsonをGetする
今回リクエスト先にはjsonplaceholder様を利用させて頂きました。
コードはこちら
package main
import (
"fmt"
"net/http"
)
func main() {
resp, err := http.Get("https://jsonplaceholder.typicode.com/posts")
fmt.Printf("%+v\n", resp)
}
こちらの実行結果は以下です。
❯ 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}
さすがに見づらいので整形します
&{
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で定義する構造体があります)
なのでこれは構造体です。
ではどんな構造体でしょう?
型を見てみましょう。コードを少し変更します。
func main() {
resp, _ := http.Get("https://jsonplaceholder.typicode.com/posts")
fmt.Printf("%T\n", resp)
}
型を取れるようにしました。実行します。
❯ go run main.go
*http.Response
httpパッケージの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 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 | マイナーバージョン |
Header
そのままですね
Body
こちらもそのままですが、ソースコードに気になる文言があります。
// Bodyを閉じるのは呼び出し側の責任である。
実は、実際にBodyの中身を利用する際には次のようなコードを書きます。
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コネクションがクローズされない」としないと書いてるのは怖いですね。
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