net/http でサーバーを立て、いくつかのパターンをパースしてみる

## はじめに
@srttk と申します。みなさまいかがお過ごしでしょうか。
私は、この投稿と同日に別のアドベントカレンダーで投稿した [contextについての記事](https://qiita.com/srttk/items/0f9e844a1775b80cdbd9) で精魂尽き果てております。興味のある方はぜひ見てくださると嬉しいです。

精魂尽き果てた結果、こっちのアドベントカレンダーに若干遅刻してしまいました、申し訳ございません。。。

これは [Gopher道場 Advent Calendar 2018 - Qiita](https://qiita.com/advent-calendar/2018/gopherdojo) の 9日目 の投稿です。
昨日は @mpppk さんの [goreleaser+go-github-selfupdateでお手軽自動リリース&アップデート - Qiita](https://qiita.com/mpppk/items/ab328356ca14938a1208) でした。
Go はとても CLI が作りやすいので、このようなリリース関連の補助ツールがあると便利です。自分で作っちゃうのがすごいですね。

さて、今回のテーマ「net/http でサーバーを立て、いくつかのパターンをパースしてみる」に至った動機ですが、
GoでAPIを作る時は、 `Gin` とか `echo` とかのサードパーティーツールを採用することが多いと思います。
個人的には、必要なものはだいたい標準パッケージで賄えると思っているんですが、やったことないのでなんとも言えないなぁと思いまして。

基本的な部分を net/http で実装してみようかな、ということでこのテーマにしました。
([Gopher道場 Advent Calendar 2018 - Qiita](https://qiita.com/advent-calendar/2018/gopherdojo) の後の投稿と内容が被りそうなので、極力 net/http パッケージの話に留めます :pray:)

このようなコードをベースに解説していこうと思います。

```go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/hello", helloHandler)

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

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "hello!\n")
}
```

## httpメソッドの受け取り

POST, PUT などのメソッドの受け取りをしてみます。
[http - The Go Programming Language](https://golang.org/pkg/net/http/#Request) をみると、
Request 構造体は Method というフィールドを持っています。これを参照すればメソッドの判断ができそうです。

```go
func helloHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "GET":
		fmt.Fprint(w, "GET hello!\n")
	case "POST":
		fmt.Fprint(w, "POST hello!\n")
	// ...省略
	default:
		fmt.Fprint(w, "Method not allowed.\n")
	}
}
```

また、これは [httpパッケージに定数が定義されて](https://github.com/golang/go/blob/11ce6eabd6073d342d57925af5bbfc0215540ddc/src/net/http/method.go#L7-L20) います。
そのため、こう書けます

```go
func helloHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		fmt.Fprint(w, "GET hello!\n")
	case http.MethodPost:
		fmt.Fprint(w, "POST hello!\n")
	// ...省略
	default:
		fmt.Fprint(w, "Method not allowed.\n")
	}
}
```

個人的には、リクエストを受け取ってからメソッドをパースできるので "method not allowed" を返しやすいのがいいですね。

## (ちょっと寄り道 ステータスコードの返却

せっかく "method not allowed" を認識できても、ステータスコードが返せないのでは締まりが悪いです。
ステータスコードを返しましょう。

[http - The Go Programming Language](https://golang.org/pkg/net/http/#ResponseWriter) をみると、
WithHeaderという関数がありました。
以下はその引用です。
>// WriteHeader sends an HTTP response header with the provided
// status code.
//
// If WriteHeader is not called explicitly, the first call to Write
// will trigger an implicit WriteHeader(http.StatusOK).
// Thus explicit calls to WriteHeader are mainly used to
// send error codes.
//
// The provided code must be a valid HTTP 1xx-5xx status code.
// Only one header may be written. Go does not currently
// support sending user-defined 1xx informational headers,
// with the exception of 100-continue response header that the
// Server sends automatically when the Request.Body is read.
WriteHeader(statusCode int)

>(コメント部google翻訳)WriteHeaderは、指定されたステータスコードでHTTP応答ヘッダーを送信します。 WriteHeaderが明示的に呼び出されない場合、Writeへの最初の呼び出しは暗黙のWriteHeader（http.StatusOK）をトリガーします。したがって、WriteHeaderへの明示的な呼び出しは、主にエラーコードの送信に使用されます。提供されるコードは有効なHTTP 1xx-5xxステータスコードでなければなりません。ヘッダーは1つだけ記述できます。 Goは現在、Request.Bodyの読み取り時にサーバーが自動的に送信する100-continueレスポンスヘッダーを除いて、ユーザー定義1xx情報ヘッダーの送信をサポートしていません。

とあります。
`WriteHeader` を使えば実装できそうです。

それぞれ正常系で終わると仮定すると、次のように書けます。

```go
func helloHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "GET hello!\n")
	case http.MethodPost:
		w.WriteHeader(http.StatusCreated)
		fmt.Fprint(w, "POST hello!\n")
	// ...省略
	default:
		w.WriteHeader(http.StatusMethodNotAllowed)
		fmt.Fprint(w, "Method not allowed.\n")
	}
}
```

これでステータスコードを返すことができました！

ちなみに、 `http.ResponseWriter` は [レスポンスHeaderを取得できる](https://github.com/golang/go/blob/master/src/net/http/server.go#L115) ので、Headerを経由しての `Set-cookie` などの登録も可能です。

## query

queryの取得には `net/url` パッケージを利用します。
ドキュメントはこちら [url - The Go Programming Language](https://golang.org/pkg/net/url)

[URL.Query](https://golang.org/pkg/net/url/#URL.Query) で [Values](https://golang.org/pkg/net/url/#Values) をとってくれば、必要な値を取得できそうです。

[http - The Go Programming Language](https://golang.org/pkg/net/http/#Request) をみると
`Request.URL` を利用すれば `*url.URL` が取得できることが確認できます。
GETの部分をこのように変更します。

```go
case http.MethodGet:
    w.WriteHeader(http.StatusOK)
    // 存在確認は省略
    name := r.URL.Query().Get("name")
    fmt.Fprintf(w, "GET hello %s!\n", name)
```

これでqueryを取得できました！

## body

次にbodyを取得していきます。
またもやパッケージドキュメントを確認すると
[http - The Go Programming Language](https://golang.org/pkg/net/http/#Request)
`Request.Body` を利用すれば `io.ReadCloser` が取れそうです。

bodyの値がjsonだとすると `encoding/json` パッケージ ([ドキュメント](https://golang.org/pkg/encoding/json/)) を利用すればパースできそうです。
(jsonパース用の型宣言が面倒なら https://mholt.github.io/json-to-go/ などを利用すると手間なくできます)

なので、JSONパース用の構造体を定義し

```go
type helloJSON struct {
	UserName string `json:"user_name"`
	Content  string `json:"content"`
}
```

このように実装します

```go
case http.MethodPost:
    body := r.Body
    defer body.Close()

    buf := new(bytes.Buffer)
    io.Copy(buf, body)

    var hello helloJSON
    json.Unmarshal(buf.Bytes(), &hello)

    w.WriteHeader(http.StatusCreated)
    fmt.Fprint(w, "POST hello! %v\n", hello)
```

curlなどで

```bash
curl -X POST -d '{"user_name":"srttk","content":"hello from srttk."}' SERVER_URI/hello
```

などとするとPOSTしたbodyが取得できていることが確認できます。

bodyとは別に、フォームから来た値は、別途 `Request.From` や `Request.PostForm` や `Request.MultipartForm` の値を利用して利用して確認することができます。

## :id などの、uriに混ざったパラメータの受け取り

今回やりたいことは、net/http の受信に極力手を加えずに、 `/ids/:id` パターンのID値を取得することです。
こんな感じです。

```go
func main() {
	http.HandleFunc("/ids/", idHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func idHandler(w http.ResponseWriter, r *http.Request) {
    // 何か処理をして ID を抽出
    id := extractID()
    fmt.Fprintf(w, "extracted id: %d\n", id)
}
```

まず、そのまま :id などをパースできる処理は net/http や net/url パッケージには(おそらく[^1])存在しませんでした。

次に gorilla/mux や gin-gonic/gin や julienschmidt/httprouter などの処理を見てみたのですが、
[URL文字列を byte ごとに探索して `:` を走査する](https://github.com/gin-gonic/gin/blob/cce49582d617333c72f4d86abd8ef0e62eb778a0/tree.go#L289) ような処理が行われており、今回やりたいこととは少し違いました。

[Goは文字列をbyteのsliceのように扱える](https://play.golang.org/p/oRHIxwUcmzG) ので、それを利用して値を抽出することにしました。

```go
func main() {
	http.HandleFunc("/ids/", idHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func idHandler(w http.ResponseWriter, r *http.Request) {
    // HandleFunc の pattern に登録されているものと同じ文字列を使用
	// 設定されている /ids/ のbyte数より後ろの文字列を取得
	s := r.URL.Path[len("/ids/"):]

	id, err := strconv.Atoi(s)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintf(w, "invalid request. %s is not ID.\n", s)
		return
	}
	fmt.Fprintf(w, "id: %d\n", id)
}
```

これで :id のような形式の値を受け取ることができました。
`"/ids/"` などがハードコードなままでは管理しづらいので、

```go
const idHandlePattern = "/ids/"
```

のようなものを定義しても良いかもしれません。

ひとまず :id をパースできましたが、これにはいくつか問題があり

- `/ids/:id/some_resources/:id` などへの対応が難しい
- [string の中身はReadOnlyなバイトスライス](https://blog.golang.org/strings#TOC_2.) なので、マルチバイト文字がURLに登場すると壊れる

などの問題があります。
これらは今回は対応できなかったので、個人的な宿題としておきます。

## まとめ

- net/httpでもだいたいできる
- が、`/:id` のようなパターンはわりとつらい

net/http を使っても、だいたいのことは可能でした。
しかし `/:id` のようなパターン[^2]は少し難しかったです。
また、標準の [http.Handle](https://golang.org/pkg/net/http/#Handle) などが受け取る `pattern` はワイルドカードなどを許容していないので、
`/ids/:id/some_resources/:id` のようなパターンをどのように対応するか[^3]はまだ見えていません。

net/http を採用することには、context や ミドルウェア の書き方についてコミュニティの見解が得られやすかったりするメリットがあります。
例えば [How I write Go HTTP services after seven years – Statuscode – Medium](https://medium.com/statuscode/how-i-write-go-http-services-after-seven-years-37c208122831) などです。

また、標準パッケージなので

- メンテナの心配をしなくてよい
- 破壊的変更などについて比較的安全
- 基本的な使い方はみんな知っていて学習コストが低い

などのメリットもあります。

しかし、その分実装の難しい部分もあります。

標準パッケージで実装することと、サードーパーティーで実装することには、どちらもメリット、デメリットがあります。
できることやできないことを認識した上で、チームにとって良い選択ができるといいですね。

[Gopher道場 Advent Calendar 2018 - Qiita](https://qiita.com/advent-calendar/2018/gopherdojo) 、明日は @matsu0228 さんです。
ぜひお楽しみに！

## 参考
[http - The Go Programming Language](https://golang.org/pkg/net/http)
[golang/go: The Go programming language](https://github.com/golang/go)
[json - The Go Programming Language](https://golang.org/pkg/encoding/json/)
[JSON-to-Go: Convert JSON to Go instantly](https://mholt.github.io/json-to-go/)
[Strings, bytes, runes and characters in Go - The Go Blog](https://blog.golang.org/strings)

[^1]: できる限り調べました。ここにあるよ！ちゃんとみてよ！などありましたら教えてください。
[^2]: gRPCとかならこの辺考慮しなくてよさそうなので、いつか試してみたいです。
[^3]: これマジでできてる方いたら教えてください。