解答 04.1 — REST設計(resolveGetTask)¶
このノートブックは 04.1-rest-design.ipynb の練習問題の解答です。
先に自分の力で解いてから、答え合わせに使ってください。
前提コード(問題ノートブックと同じ定義)¶
In [1]:
import "fmt"
// Task はこのレッスンで扱う唯一のリソース。
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
解答: resolveGetTask¶
「見つからない」は error ではなく、404 という正常なステータスコードで表現します。
In [2]:
// resolveGetTask は store に id が存在すれば (Task, 200, nil) を返す。
// 存在しなければ (Task{}, 404, nil) を返す(404 は正常なレスポンス。error にはしない)。
func resolveGetTask(store map[int]Task, id int) (Task, int, error) {
t, ok := store[id]
if !ok {
return Task{}, 404, nil
}
return t, 200, nil
}
チェック(問題ノートブックと同じ期待値)¶
In [3]:
import "reflect"
import "fmt"
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]:
%%
store := map[int]Task{1: {ID: 1, Title: "牛乳を買う", Done: false}}
task, status, err := resolveGetTask(store, 1)
mustEqual(err, nil, "存在するIDはエラーなし")
mustEqual(status, 200, "存在するIDは200")
mustEqual(task, Task{ID: 1, Title: "牛乳を買う", Done: false}, "取得したTaskの中身が一致")
task2, status2, err2 := resolveGetTask(store, 999)
mustEqual(err2, nil, "存在しないIDでもエラーは返さない(404は正常応答)")
mustEqual(status2, 404, "存在しないIDは404")
mustEqual(task2, Task{}, "存在しないIDはゼロ値のTask")
println("🎉 すべてのチェックが通りました")
✅ Passed: 存在するIDはエラーなし ✅ Passed: 存在するIDは200 ✅ Passed: 取得したTaskの中身が一致 ✅ Passed: 存在しないIDでもエラーは返さない(404は正常応答) ✅ Passed: 存在しないIDは404 ✅ Passed: 存在しないIDはゼロ値のTask
🎉 すべてのチェックが通りました
解説¶
- 戻り値が
(Task, int, error)で、errorは「見つからない」ではnilのまま — これがこのレッスンの核心です。404は例外的な失敗ではなく、「そのIDのリソースは無い」という 正常に判定できた結果なので、Go のerrorを使う理由がありません t, ok := store[id]のカンマ ok イディオム — マップにidが無い場合、okはfalseに、tはゼロ値(Task{})になります。この「無い」をokで判定してから404を組み立てます- 呼び出し側(HTTPハンドラ)は
resolveGetTaskの戻り値をそのままwriteJSON(w, status, task)のように使えます。ステータスコードの決定ロジックと、HTTPの配線(net/http)を分離できるのが、 このような純粋関数として実装する利点です(Module 4 のこの後のレッスンで扱う「層を分ける」設計の一歩目です)