🔰

Go 標準パッケージ理解 net/http編

に公開

前置き

  • 自分の理解用
  • 学習兼記事進捗メモ
    • httpクライアント 〇
    • httpサーバー 未
    • コンテキスト 未
    • jsonエンコードデコード △
    • io.Reader △
    • ミドルウェアとか...etc 未

net/httpとは

  • Goで用意されている標準パッケージの1つ
  • HTTP関連の型や関数を提供する
  • 製品レベルのHTTP/2クライアントとサーバーが含まれている

HTTPクライアント

構造体http.Clientが定義されており、HTTPリクエストの生成とレスポンスの受信ができる。

デフォルトクライアント

デフォルトのクライアント(net/httpに含まれている)では、タイムアウトが無いため、本番環境での利用は推奨されていない。

http.Get関数もDefaultClientを利用している(GetはDefautlClient.Getのラッパー)

// デフォルトのクライアントを使ったHTTP応答
package main

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

type DogResponse struct {
	Message string `json:"message"`
	Status  string `json:"status"`
}

resp, err := http.DefaultClient.Get("https://dog.ceo/api/breeds/image/random")
// resp, err := http.Get("https://dog.ceo/api/breeds/image/random")
if err != nil {
    // エラーハンドリング
}

defer resp.Body.Close()

var dogResponse DogResponse
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&dogResponse); err != nil {
    // エラーハンドリング
}

fmt.Println(dogResponse.Message)

開発環境はデフォルトのクライアントを使うという考え方もあるが、個人的には独自のクライアントインスタンスを作った方がいいと思う。

  1. 書き方に慣れる
  2. 本番環境と開発環境のコードの差異が少なくなる

以下では、明示的にクライアントのインスタンスを作成している。
プログラム全体でhttp.Clientをひとつだけ作成する。

client := http.Client{
    Timeout: 30 * time.Second, // タイムリミットの設定  30秒
}

リクエスト

*http.Requestインスタンスの作成

リクエストを送りたい時には、http.NewRequestWithContext関数を使って新しい*http.Requestのインスタンスを作成する

  • *http.Request = 構造体Requestへのアクセス

  • コンテキスト、メソッド、接続先URLを渡す

  • PUTPOSTPATCHのいずれかのリクエストの場合、最後の引数をio.ReaderとしてリクエストのBodyを指定する

    • Bodyが無い場合はnilを指定する(つまりGetの時)
  1. context.Background()はまだコンテキスト未勉強。勉強したら加筆するかも
  2. http.MethodGet定数
  3. urlはそのまんま変数url
  4. nilは上記でも記述したように、Bodyが無い(Get)ためnilとしている
    • id.ReaderBodyを指定する
    • ぶっちゃけid.Readerはよく分かってない。勉強したら加筆するかも。
url := "https://dog.ceo/api/breeds/image/random"
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil {
    fmt.Printf("リクエスト%d回目", i)
    fmt.Printf("リクエストの作成に失敗しました: %v", err)
}

ヘッダーの設定、リクエストの送信と結果格納

*http.Requestのインスタンスを作成したら、http.Requestを使ってhttp.Clientを呼び出す

  1. http.ClientのメソッドはDoを指定する
  2. http.Responseに結果が返る(func (*Client)Do(*Response, error)の部分)

ヘッダー追加すると何ができるか、何が嬉しいのかは未勉強。
認証、キャッシュ、クッキー用のヘッダーとかかなぁと今のところイメージしてる。

req.Header.Add("X-My-Client", "Go-Learn") // リクエストのヘッダーに追加
res, err := client.Do(req) // リクエストを送信して結果を格納
if err != nil {
    fmt.Printf("リクエストの作成に失敗しました: %v", err)
}

レスポンスの確認、jsonのデコード

  1. レスポンスのステータスコードはフィールドStatusCodeに格納される
  2. レスポンスのテキストはStatusに格納される
  3. レスポンスヘッダーはHeaderに格納される
    • Responseの構造体で受け取り、HeaderフィールドはHeader型のマップを参照している
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
    fmt.Printf("APIからエラーステータスが返されました: %s", res.Status)
}
fmt.Println(res.Header.Get("Content-Type")) // 結果確認用。不要ならコメントアウト

var data Data
type Data struct {
	Message string `json:"message"`
	Status  string `json:"status"`
}

err = json.NewDecoder(res.Body).Decode(&data)
if err != nil {
    fmt.Printf("JSONのデコードに失敗しました: %v", err)
}

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

理解するためのメモ

  1. Bodyio.ReadCloser型のフィールドBodyに格納される
  2. jsonパッケージのデコーダを使うことで、簡単にレスポンスを処理することができる
  3. defer res.Body.Close()
    1. 接続のクローズ。deferなのでプログラムの最後(return)のタイミングで実行される
    2. ループ文を処理するときはdeferステートメントを使わず、関数が終了する直前に実行するようにすべきとのこと(他の関数に処理を切り出すとdeferはいい感じに使えるとのこと)
      • 接続がスタックして閉じない状態になり、接続数が跳ね上がりやすくなる
  4. if res.StatusCode != http.StatusOK
    1. レスポンスのステータスコードがステータスOK(200)じゃなければエラー出力を行う
    2. StatusOKは定数
  5. jsonタグは可読性を向上させる(ほかにも理由があるらしいがまだ詳しくは見れてない)
    • フィールドはint型、bool型など色々指定可能
      var data Data
      type Data struct {
          Message string `json:"message"`
          Status  string `json:"status"`
      }
      
  6. err = json.NewDecoder(res.Body).Decode(&data)
    • jsonパッケージのNewDecoderメソッドにres.Bodyを引数として渡しつつ、Decodeメソッドに&dataを引数として渡している・・・という理解
      1. NewDecoderjsonのデコーダを作成
      2. Decode:作成されたjsonデコーダに対してデコードを実施
      3. NewDecoderJsonを読み取るためのデコーダ(解読器)を作成し、デコーダのDecodeメソッドでGo内で作った構造体に格納しているって感じ?

結果

application/json
{Message:https://images.dog.ceo/breeds/pembroke/n02113023_5985.jpg Status:success}

参考

GitHubで編集を提案

Discussion