Go言語で薄く作るAPI(go-chi/chi) #2 カスタムハンドラ
GoっぽくAPIを作る
前回
Go言語で薄く作るAPI(go-chi/chi) #1 最低限構築 - ぺい
今回
- カスタムハンドラ
もっと書く予定だったのですが、ハンドラだけですごいボリュームなったので、ハンドラだけにしましたw
go-chi-api-example/02 at master · pei0804/go-chi-api-example · GitHub
$ go get github.com/pei0804/go-chi-api-example $ cd $GOPATH/src/github.com/pei0804/go-chi-api-example/02 $ make run go build -o build . && ./build 2017/11/26 00:05:04 Starting app $ make curl-members-id id=01 curl -H 'Auth:admin' http://localhost:8080/api/members/01 { "id": 1, "name": "name_1" }
カスタムハンドラ
カスタムハンドラを導入することで、コントローラー(以降ハンドラとする)の実装が少し変わります。まずは、以下のソースを見てください。
カスタムハンドラなし
// Show endpoint func (h *Handler) Show(w http.ResponseWriter, r *http.Request) { type json struct { ID int `json:"id"` Name string `json:"name"` } id, _ := strconv.Atoi(chi.URLParam(r, "id")) res := json{ID: id, Name: fmt.Sprint("name_", id)} respondJSON(w, http.StatusOK, res) } // respondJSON レスポンスとして返すjsonを生成して、writerに書き込む func respondJSON(w http.ResponseWriter, status int, payload interface{}) { response, err := json.MarshalIndent(payload, "", " ") if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) w.Write([]byte(response)) }
普通に実装すると、type HandlerFunc func(ResponseWriter, *Request)
の型で実装して、ハンドラを作成しています。net/http
パッケージの実装には以下のように書かれています。
// The HandlerFunc type is an adapter to allow the use of // ordinary functions as HTTP handlers. If f is a function // with the appropriate signature, HandlerFunc(f) is a // Handler that calls f. type HandlerFunc func(ResponseWriter, *Request)
カスタムハンドラあり
// Show endpoint func (c *Controller) Show(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { id, _ := strconv.Atoi(chi.URLParam(r, "id")) res := User{ID: id, Name: fmt.Sprint("name_", id)} return http.StatusOK, res, nil }
カスタムハンドラを使った実装をしているハンドラは、type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error)
という型で実装しています。では、これは何に準拠された実装になるのか?また何が嬉しいのか?
実はこれも、type HandlerFunc func(ResponseWriter, *Request)
に最終的に合わせて処理をしています。それを理解するには、もう少しソースを追いかける必要があります。
func (s *Server) Router() { c := NewController() s.router.Route("/api", func(api chi.Router) { api.Use(Auth("db connection")) api.Route("/members", func(members chi.Router) { members.Get("/{id}", handler(c.Show).ServeHTTP) // ここでハンドラが呼ばれる members.Get("/", handler(c.List).ServeHTTP) }) }) s.router.Route("/api/auth", func(auth chi.Router) { auth.Get("/login", handler(c.Login).ServeHTTP) }) }
members.Get("/{id}", handler(c.Show).ServeHTTP)
というのを一つずつ分解すると、
Get(pattern string, h http.HandlerFunc)
chi
パッケージのメソッドpattern
はエンドポイントになる文字列http.HandlerFunc
はtype HandlerFunc func(ResponseWriter, *Request)
handler(c.Show).ServeHTTP
type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error)
という今回作成した型c.Show
ハンドラc.
はtype Controller struct
を指し、
Show
はfunc (c *Controller) Show(http.ResponseWriter, *http.Request) (int, interface{}, error)
ですServeHTTP
はfunc (handler) ServeHTTP(http.ResponseWriter, *http.Request)
というメソッドを呼んでいます
つまり
Get(pattern string, h http.HandlerFunc)
の型にハマるように、ハンドラを作成する。
逆にいうと、カスタムしなければ、members.Get("/{id}", h.Show)
とそのまま格納しても問題なく動きます。
逆にカスタムハンドラの方は、members.Get("/{id}", handler(c.Show).ServeHTTP)
というように少し書き方が変わっています。重要なのは、ここです。
ServeHTTPが重要
type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error) func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer func() { // panic対応 if rv := recover(); rv != nil { debug.PrintStack() log.Printf("panic: %s", rv) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }() // h(w, r)はhandler型のハンドラの実行 // func(http.ResponseWriter, *http.Request) (int, interface{}, error) status, res, err := h(w, r) if err != nil { log.Printf("error: %s", err) respondError(w, status, err) return } respondJSON(w, status, res) return }
ServeHTTP
はhandler
という型から使えるメソッドです。
ServeHTTP
はfunc ServeHTTP(http.ResponseWriter, *http.Request)
という型のメソッドです。
そして、status, res, err := h(w, r)
で、c.Show()
が実行されています。自分はここで何が起こっているのか、理解に時間がかかりました。
ちなみに、Method(method, pattern string, h http.Handler)
というメソッドを使う場合は、Method("GET", "/{id}", handler(c.Show))
と書くだけでいい感じに実行されます。何故か?
// A Handler responds to an HTTP request. // // ServeHTTP should write reply headers and data to the ResponseWriter // and then return. Returning signals that the request is finished; it // is not valid to use the ResponseWriter or read from the // Request.Body after or concurrently with the completion of the // ServeHTTP call. // // Depending on the HTTP client software, HTTP protocol version, and // any intermediaries between the client and the Go server, it may not // be possible to read from the Request.Body after writing to the // ResponseWriter. Cautious handlers should read the Request.Body // first, and then reply. // // Except for reading the body, handlers should not modify the // provided Request. // // If ServeHTTP panics, the server (the caller of ServeHTTP) assumes // that the effect of the panic was isolated to the active request. // It recovers the panic, logs a stack trace to the server error log, // and hangs up the connection. type Handler interface { ServeHTTP(ResponseWriter, *Request) }
net/http
パッケージには、Handler
というinterface
があり、これに準拠しているメソッドを定義している場合、Handler
として使えるという記述がある。しかし、HanderFunc
の場合は、そういったインターフェースは用意されていないので、最終的にHandlerFunc
の型に合わせる。
そのため、members.Get("/{id}", handler(c.Show).ServeHTTP)
と書くに至る。
カスタムハンドラを作ると何が嬉しい?
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 前後自由に書ける status, res, err := h(w, r) // 前後自由に書ける }
ざっくり言うと、上みたいな感じでハンドラは前後でやりたいことを書くことが出来ます。案件に合わせた柔軟なハンドラを作れるので、かなり重宝します。 他にも、入口と出口のルールづくりが出来るので、必ずこの値がほしい、必ずこの値を返してほしいというインターフェース的なものが出来るので、忘れて本来やるべきだったものをやっていなかったということを防げます。例えば、以下のようなものです。
// Show endpoint func (h *Handler) Show(w http.ResponseWriter, r *http.Request) { type json struct { ID int `json:"id"` Name string `json:"name"` } id, _ := strconv.Atoi(chi.URLParam(r, "id")) res := json{ID: id, Name: fmt.Sprint("name_", id)} response, err := json.MarshalIndent(payload, "", " ") if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } // 以下の2行を書き忘れるても、処理は正常に終わってしまうなど // w.Header().Set("Content-Type", "application/json") // w.WriteHeader(status) w.Write([]byte(response)) }
カスタムハンドラは非常に便利なので、必要な入り口と必要な出口を案件に合わせて定義すると良さそうです。この記事を書く上で、記事を読んだり、GitHub上のソースを見てハンドラを参考にしたり、自分なりに考えてヘルパーを作ったりしたのですが、結局使えないというドツボにハマったので、シンプルに最低限必要なもので対応するのが良さそうです。
一応解説しておくと、今回のハンドラは以下のように使います。
func(http.ResponseWriter, *http.Request) (int, interface{}, error)
int
HTTPステータスコードinterface{}
レスポンスに使うJSONなど(今回はJSONだけを想定)error
何かしらのエラー
次回
エラーハンドリングについて書く予定です