net/httpによるHTTPメソッドを含んだルーティングの実装

最近GoによるWebアプリケーション開発を学び始めたので間違っている箇所があればコメントください。

ServeMux型によるルーティング

http.Handle関数を使うとパスに対するルーティングを登録することができる。http.Handler型は実際にリクエストを処理するオブジェクトで、下のように実装すると/foodsへのリクエストを*FoodsHandler型が処理することになる。

http.Handle("/foods", &handlers.FoodsHandler{})

http.Handle関数によって登録されたルーティングはhttp.DefaultServeMuxという*ServeMux型の変数が保持することになる。

type ServeMux struct {
    mu sync.RWMutex
    m map[string]muxEntry
    hosts bool
}

type muxEntry struct {
    h Handler
    pattern string
}

登録されたルーティングはフィールドmで保持される。サーバーはmから一致するパスを探し、対応するHandlerを呼び出す。

見たところ、ServeMux型ではGET, POST等のHTTPメソッドを考慮していない。RESTful APIを実装するにはHTTPメソッドを考慮する必要があるため、ServeMux型によるルーティングでは不十分だと分かる。そこで、ルーティングを自前で実装する。

Handlerによるルーティング

http.Handle関数の代わりにhttp.ListenAndServe関数に渡すhttp.Handlerによってルーティングを実装する。

http.ListenAndServe(":8080", handler)

http.DefaultServeMuxを使う場合はhandlerの代わりにnilを渡すが、自前のハンドラーを使う場合はここに渡す。

type RoutesHandler struct {
    routes map[string]map[string]http.Handler
}

func (h *RoutesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    paths, ok := h.routes[r.Method]
    if !ok {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    handler, ok := paths[r.URL.Path]
    if !ok {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    handler.ServeHTTP(w, r)
}

*ServeMux型とは違い、map[string]map[string]http.Handler型のフィールドroutesでHTTPメソッドを含むルーティングを管理するようにした。ServeHTTP関数を実装することでhttp.Handler型のインターフェイスを満たしている。内部でroutesから一致するハンドラーを呼び出す。

func (h *RoutesHandler) GET(path string, handler http.Handler) {
    h.register("GET", path, handler)
}

func (h *RoutesHandler) POST(path string, handler http.Handler) {
    h.register("POST", path, handler)
}

func (h *RoutesHandler) register(method, path string, handler http.Handler) {
    if h.routes == nil {
        h.routes = make(map[string]map[string]http.Handler)
    }

    _, ok := h.routes[method]
    if !ok {
        h.routes[method] = make(map[string]http.Handler)
    }

    h.routes[method][path] = handler
}

こうした関数を定義し、ルーティングを登録できるようにする。

routesHandler := &handlers.RoutesHandler{}
routesHandler.GET("/foods", &handlers.FoodsHandler{})

http.ListenAndServe(":8080", routesHandler)