🔩 ねじき教室 Go と Web の教室

リクエストを受け取る

読了目安 約3分

パスパラメータ、クエリ文字列、ボディの 3 経路でクライアントからデータを受け取り、検証して 400 を返す判断を身につけます。

この章の目次

クライアントからデータを受け取る経路は主に 3 つ、パスパラメータ、クエリ文字列、ボディです。 3 つとも実装した次の内容を main.go に保存し、go run main.go で起動します。

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/posts

json.NewDecoder(r.Body).Decode(&p) が、ボディの JSON を struct に流し込みます。 フィールド後ろの `json:"body"` は JSON のキーとの対応を指定する注釈で、次章で説明します。

r.Body は一度しか読めないストリームです。

受け取った値は必ず検証する

どの経路の値も、どんな文字列でも送れてしまう以上、信用できません。 「整数のはず」「空でないはず」はサーバー側で確かめます。

シェル
curl -i http://localhost:8080/users/abc
テキスト
HTTP/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 番台は、次章で扱います。