リクエストを受け取る
読了目安 約3分
パスパラメータ、クエリ文字列、ボディの 3 経路でクライアントからデータを受け取り、検証して 400 を返す判断を身につけます。
この章の目次
クライアントからデータを受け取る経路は主に 3 つ、パスパラメータ、クエリ文字列、ボディです。
3 つとも実装した次の内容を main.go に保存し、go run main.go で起動します。
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
)
func main() {
mux := http.NewServeMux()
// 経路 1: パスパラメータ
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "id は整数で指定してください", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "ユーザー ID: %d\n", id)
})
// 経路 2: クエリ文字列
mux.HandleFunc("GET /search", func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
if q == "" {
http.Error(w, "検索語 q を指定してください", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "検索語: %s\n", q)
})
// 経路 3a: ボディ(フォーム)
mux.HandleFunc("POST /posts", func(w http.ResponseWriter, r *http.Request) {
body := r.FormValue("body")
if body == "" {
http.Error(w, "本文 body が空です", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "投稿を受け付けました: %s\n", body)
})
// 経路 3b: ボディ(JSON)
mux.HandleFunc("POST /api/posts", func(w http.ResponseWriter, r *http.Request) {
var p struct {
Body string `json:"body"`
}
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, "JSON を解釈できません", http.StatusBadRequest)
return
}
if p.Body == "" {
http.Error(w, "本文 body が空です", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "投稿を受け付けました: %s\n", p.Body)
})
log.Fatal(http.ListenAndServe(":8080", mux))
}経路 1: パスパラメータ
curl http://localhost:8080/users/42{id} に合致した部分を r.PathValue("id") で取り出します。
「どの資源か」を URL で指す用途に向いています。
経路 2: クエリ文字列
クエリ文字列は、URL の ? より後ろに キー=値 を & でつないで並べる形式です。
「同じ資源への注文の違い」を表します。
curl 'http://localhost:8080/search?q=go'値は r.URL.Query().Get("q") で取り出します。
未指定なら空文字列が返ります。
?や&はシェルの特殊文字なので、URL をシングルクォートで囲んでいます。
経路 3: ボディ
URL に載せられる量には限りがあり、履歴やログにも残るため、投稿の本文やパスワードは POST リクエストのボディに入れて送ります。 ボディはヘッダーの後ろに続くデータ本体で、形式は主に 2 つです。
1 つ目は HTML のフォームが送る キー=値 形式です。
ボディは curl の -d で指定し、フォーム形式の Content-Type ヘッダーも自動で付きます。
curl -X POST -d "body=はじめての投稿" http://localhost:8080/postsサーバー側は r.FormValue("body") で値を取り出せます。
2 つ目は HTML と JSON で読んだ JSON で、Web API では主流です。 JSON では Content-Type ヘッダーを自分で指定します。
curl -X POST -H "Content-Type: application/json" \
-d '{"body":"はじめての投稿"}' http://localhost:8080/api/postsjson.NewDecoder(r.Body).Decode(&p) が、ボディの JSON を struct に流し込みます。
フィールド後ろの `json:"body"` は JSON のキーとの対応を指定する注釈で、次章で説明します。
r.Bodyは一度しか読めないストリームです。
受け取った値は必ず検証する
どの経路の値も、どんな文字列でも送れてしまう以上、信用できません。 「整数のはず」「空でないはず」はサーバー側で確かめます。
curl -i http://localhost:8080/users/abcHTTP/1.1 400 Bad Request
...
id は整数で指定してくださいr.PathValue("id") が返すのは文字列 "abc" です。
strconv.Atoi は変換できない文字列にエラーを返すので、エラー処理の if err != nil がそのまま検証になります。
壊れた値は、深い場所で失敗する前に入口で止めるのが最も安く済みます。
http.Error は、指定したステータスコードとメッセージを 1 行で書き出す関数です。
直後の return を忘れると、エラーを返したのに処理が続いてしまいます。
400 を返す判断
ステータスコードの 4xx は「クライアント側の誤り」、5xx は「サーバー側の失敗」です。 整数のはずが文字列、必須の値が空など、仕様に合わないリクエストには 400 Bad Request を返します。 リクエストは正しいのにサーバー側が失敗したときの 500 番台は、次章で扱います。