解答 03.2 — HTTP解剖(RequestStatus)¶
このノートブックは 03.2-http-anatomy.ipynb の練習問題の解答です。
まず問題の前提コードを再掲し、次に解答、最後にチェックを実行します。
先に自分の力で解いてから、答え合わせに使ってください。
前提コード(問題ノートブックと同じ定義)¶
In [1]:
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
)
// ErrUnanswered は、練習問題が未回答のときにプレースホルダ関数が返す特別なエラー。
var ErrUnanswered = errors.New("未回答: この関数はまだ実装されていません")
// newTestServer は、/users(一覧取得・新規作成)と /users/{id}(単体取得)を持つ
// テスト用サーバーを起動する。
func newTestServer() *httptest.Server {
mux := http.NewServeMux()
users := map[int]string{1: "tanaka", 2: "suzuki"}
nextID := 3
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
var b strings.Builder
b.WriteString("[")
first := true
for id := 1; id < nextID; id++ {
name, ok := users[id]
if !ok {
continue
}
if !first {
b.WriteString(",")
}
fmt.Fprintf(&b, `{"id":%d,"name":"%s"}`, id, name)
first = false
}
b.WriteString("]")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(b.String()))
case http.MethodPost:
var payload struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error":"invalid json"}`))
return
}
id := nextID
users[id] = payload.Name
nextID++
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Location", fmt.Sprintf("/users/%d", id))
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `{"id":%d,"name":"%s"}`, id, payload.Name)
default:
w.Header().Set("Allow", "GET, POST")
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte(`{"error":"method not allowed"}`))
}
})
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte(`{"error":"method not allowed"}`))
return
}
idStr := strings.TrimPrefix(r.URL.Path, "/users/")
id, err := strconv.Atoi(idStr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error":"invalid id"}`))
return
}
name, ok := users[id]
if !ok {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error":"not found"}`))
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"id":%d,"name":"%s"}`, id, name)
})
return httptest.NewServer(mux)
}
解答: RequestStatus¶
http.NewRequest でリクエストを組み立て、srv.Client().Do で送るだけです。
In [2]:
func RequestStatus(srv *httptest.Server, method, path string, body io.Reader) (int, error) {
req, err := http.NewRequest(method, srv.URL+path, body)
if err != nil {
return 0, err
}
resp, err := srv.Client().Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
return resp.StatusCode, nil
}
チェック(問題ノートブックと同じ期待値)¶
答え合わせ用のヘルパー mustEqual を定義します。
In [3]:
import "reflect"
func mustEqual(got, want any, name string) {
if reflect.DeepEqual(got, want) {
fmt.Printf("✅ Passed: %s\n", name)
return
}
panic(fmt.Sprintf("❌ %s\n got = %v (%T)\n want = %v (%T)", name, got, got, want, want))
}
In [4]:
%%
srv := newTestServer()
defer srv.Close()
status1, err1 := RequestStatus(srv, http.MethodGet, "/users", nil)
if err1 != nil {
panic(err1)
}
mustEqual(status1, http.StatusOK, "GET /users は200")
status2, err2 := RequestStatus(srv, http.MethodGet, "/users/999", nil)
if err2 != nil {
panic(err2)
}
mustEqual(status2, http.StatusNotFound, "GET /users/999 は404")
status3, err3 := RequestStatus(srv, http.MethodPost, "/users", strings.NewReader(`{"name":"kimura"}`))
if err3 != nil {
panic(err3)
}
mustEqual(status3, http.StatusCreated, "POST /users は201")
status4, err4 := RequestStatus(srv, http.MethodDelete, "/users", nil)
if err4 != nil {
panic(err4)
}
mustEqual(status4, http.StatusMethodNotAllowed, "DELETE /users は405")
fmt.Println("🎉 すべてのチェックが通りました")
✅ Passed: GET /users は200
✅ Passed: GET /users/999 は404
✅ Passed: POST /users は201
✅ Passed: DELETE /users は405
🎉 すべてのチェックが通りました
解説¶
http.NewRequest+Client.Do:http.Get/http.Postと違い、メソッドを自由に指定できる 汎用的な組み立て方です。ヘッダーを付けたい場合もreq.Header.Set(...)が使えます- ステータスコードだけを見れば十分な場面: ボディの中身を検証したいわけではなく「結果の種類」
だけを確認したい場合、
resp.StatusCodeだけを返す軽量な関数にする設計は実務でもよく使います