ぺい

渋谷系アドテクエンジニアの落書き

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.HandlerFunctype HandlerFunc func(ResponseWriter, *Request)

handler(c.Show).ServeHTTP

  • type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error) という今回作成した型
  • c.Show ハンドラc.type Controller structを指し、
    Showfunc (c *Controller) Show(http.ResponseWriter, *http.Request) (int, interface{}, error)です
  • ServeHTTPfunc (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
}

ServeHTTPhandlerという型から使えるメソッドです。
ServeHTTPfunc 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)

次回

エラーハンドリングについて書く予定です