はじめに
@srttk と申します。みなさまいかがお過ごしでしょうか。
私は、この投稿と同日に別のアドベントカレンダーで投稿した contextについての記事 で精魂尽き果てております。興味のある方はぜひ見てくださると嬉しいです。
精魂尽き果てた結果、こっちのアドベントカレンダーに若干遅刻してしまいました、申し訳ございません。。。
これは Gopher道場 Advent Calendar 2018 - Qiita の 9日目 の投稿です。
昨日は @mpppk さんの goreleaser+go-github-selfupdateでお手軽自動リリース&アップデート - Qiita でした。
Go はとても CLI が作りやすいので、このようなリリース関連の補助ツールがあると便利です。自分で作っちゃうのがすごいですね。
さて、今回のテーマ「net/http でサーバーを立て、いくつかのパターンをパースしてみる」に至った動機ですが、
GoでAPIを作る時は、 Gin
とか echo
とかのサードパーティーツールを採用することが多いと思います。
個人的には、必要なものはだいたい標準パッケージで賄えると思っているんですが、やったことないのでなんとも言えないなぁと思いまして。
基本的な部分を net/http で実装してみようかな、ということでこのテーマにしました。
(Gopher道場 Advent Calendar 2018 - Qiita の後の投稿と内容が被りそうなので、極力 net/http パッケージの話に留めます )
このようなコードをベースに解説していこうと思います。
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 をみると、
Request 構造体は Method というフィールドを持っています。これを参照すればメソッドの判断ができそうです。
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パッケージに定数が定義されて います。
そのため、こう書けます
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 をみると、
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
を使えば実装できそうです。
それぞれ正常系で終わると仮定すると、次のように書けます。
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を取得できる ので、Headerを経由しての Set-cookie
などの登録も可能です。
query
queryの取得には net/url
パッケージを利用します。
ドキュメントはこちら url - The Go Programming Language
URL.Query で Values をとってくれば、必要な値を取得できそうです。
http - The Go Programming Language をみると
Request.URL
を利用すれば *url.URL
が取得できることが確認できます。
GETの部分をこのように変更します。
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
Request.Body
を利用すれば io.ReadCloser
が取れそうです。
bodyの値がjsonだとすると encoding/json
パッケージ (ドキュメント) を利用すればパースできそうです。
(jsonパース用の型宣言が面倒なら https://mholt.github.io/json-to-go/ などを利用すると手間なくできます)
なので、JSONパース用の構造体を定義し
type helloJSON struct {
UserName string `json:"user_name"`
Content string `json:"content"`
}
このように実装します
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などで
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値を取得することです。
こんな感じです。
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 ごとに探索して :
を走査する ような処理が行われており、今回やりたいこととは少し違いました。
Goは文字列をbyteのsliceのように扱える ので、それを利用して値を抽出することにしました。
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/"
などがハードコードなままでは管理しづらいので、
const idHandlePattern = "/ids/"
のようなものを定義しても良いかもしれません。
ひとまず :id をパースできましたが、これにはいくつか問題があり
-
/ids/:id/some_resources/:id
などへの対応が難しい - string の中身はReadOnlyなバイトスライス なので、マルチバイト文字がURLに登場すると壊れる
などの問題があります。
これらは今回は対応できなかったので、個人的な宿題としておきます。
まとめ
- net/httpでもだいたいできる
- が、
/:id
のようなパターンはわりとつらい
net/http を使っても、だいたいのことは可能でした。
しかし /:id
のようなパターン2は少し難しかったです。
また、標準の http.Handle などが受け取る pattern
はワイルドカードなどを許容していないので、
/ids/:id/some_resources/:id
のようなパターンをどのように対応するか3はまだ見えていません。
net/http を採用することには、context や ミドルウェア の書き方についてコミュニティの見解が得られやすかったりするメリットがあります。
例えば How I write Go HTTP services after seven years – Statuscode – Medium などです。
また、標準パッケージなので
- メンテナの心配をしなくてよい
- 破壊的変更などについて比較的安全
- 基本的な使い方はみんな知っていて学習コストが低い
などのメリットもあります。
しかし、その分実装の難しい部分もあります。
標準パッケージで実装することと、サードーパーティーで実装することには、どちらもメリット、デメリットがあります。
できることやできないことを認識した上で、チームにとって良い選択ができるといいですね。
Gopher道場 Advent Calendar 2018 - Qiita 、明日は @matsu0228 さんです。
ぜひお楽しみに!
参考
http - The Go Programming Language
golang/go: The Go programming language
json - The Go Programming Language
JSON-to-Go: Convert JSON to Go instantly
Strings, bytes, runes and characters in Go - The Go Blog