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

レスポンスを返す

読了目安 約3分

ステータスコードとヘッダーの書き方を押さえ、JSON を返す API と html/template で HTML を返すページを実装します。

この章の目次

レスポンスの 3 要素、ステータスコード、ヘッダー、ボディを制御して、JSON の API と HTML のページを作ります。

ステータスコードとヘッダーを書く

http.ResponseWriter には、ボディの書き込みのほかに 2 つの操作があります。

Go
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusCreated) // 201
fmt.Fprintln(w, "作成しました")

w.Header().Set はヘッダーを設定します。 Content-Type はボディの形式を伝えるヘッダーで、ブラウザは描画方法をこれで決めます。

w.WriteHeader はステータスコードを指定します。 呼ばずにボディを書き始めると、自動で 200 OK になります。

WriteHeader の瞬間にステータス行とヘッダーが送信されて取り消せないため、「ヘッダー、ステータス、ボディ」の順で書きます。

リクエストは正しいのに処理が失敗したときは、500 Internal Server Error です。

Go
http.Error(w, "サーバー内部でエラーが発生しました", http.StatusInternalServerError)

JSON を返す API と HTML を返すページ

次を main.go に保存してください。

Go
package main

import (
	"encoding/json"
	"html/template"
	"log"
	"net/http"
)

type Post struct {
	ID   int    `json:"id"`
	Body string `json:"body"`
}

var tmpl = template.Must(template.New("page").Parse(`<!DOCTYPE html>
<html>
<body>
  <h1>{{.Title}}</h1>
  <p>メッセージ: {{.Message}}</p>
</body>
</html>
`))

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /api/posts", func(w http.ResponseWriter, r *http.Request) {
		posts := []Post{
			{ID: 1, Body: "最初の投稿"},
			{ID: 2, Body: "二つ目の投稿"},
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(posts)
	})

	mux.HandleFunc("GET /page", func(w http.ResponseWriter, r *http.Request) {
		data := struct {
			Title   string
			Message string
		}{
			Title:   "ねじき教室",
			Message: r.URL.Query().Get("msg"),
		}
		tmpl.Execute(w, data)
	})

	log.Fatal(http.ListenAndServe(":8080", mux))
}

go run main.go で起動し、まず API を叩きます。

シェル
curl -i http://localhost:8080/api/posts
テキスト
HTTP/1.1 200 OK
Content-Type: application/json
...

[{"id":1,"body":"最初の投稿"},{"id":2,"body":"二つ目の投稿"}]

HTML と JSON で読んだ JSON を、今度は生成する側です。

struct を JSON に変換する

json.NewEncoder(w).Encode(posts) は、Go の値を JSON に変換して w に書き込みます(前章の Decode の逆)。

キー名を決めるのが、フィールド定義の後ろの struct タグです。

Go
ID int `json:"id"`

タグがなければフィールド名("ID")がそのままキーになります。 Go は公開フィールドが大文字始まり、JSON のキーは小文字が通例で、タグがその橋渡しです。

テンプレートで HTML を組み立てる

ブラウザで http://localhost:8080/page?msg=こんにちは を開くと、「メッセージ: こんにちは」を含む HTML が表示されます。

テンプレートは、穴あきの雛形に値を埋め込む仕組みです。 {{.Title}} のような穴を含む雛形を template.Must(...Parse(...)) で用意し、tmpl.Execute(w, data) を呼ぶと、穴に data の値を埋めた結果が w に書き出されます。

穴とデータの対応は名前で決まるので、Execute には TitleMessage を持つ無名 struct を渡しています。

template.Must は、雛形の構文が壊れていたら起動時に panic させる包み紙です。

text/template ではなく html/template を使う理由

兄弟 text/template ではなく html/template を選ぶ理由は、エスケープ(記号を無害な表記への変換)が自動だからです。

msg の値(前章の「信用できない値」)が、そのまま HTML に埋め込まれます。 次を叩いてみてください。

シェル
curl 'http://localhost:8080/page?msg=<script>alert(1)</script>'

出力では <&lt; に変換されており、ブラウザはただの文字として表示するだけです。

text/template なら <script> がそのまま出力され、閲覧者のブラウザで他人のスクリプトが動いてしまいます。 これが代表的な攻撃 XSS(クロスサイトスクリプティング)です。 「HTML は html/template で出す」を機械的に守るだけで、大部分を防げます。