Goの標準パッケージだけでRESTfulなHandlerを作る

Goのnet/httpパッケージだけではパスパラメータを扱うことができないため、/users/:id のようなエンドポイントを作ろうとしたら自分で処理を書かなければいけない。
適当なフレームワークや3rd-partyのパッケージを使えば簡単ではあるんだけど、時々標準パッケージだけで書きたくなってその度にどうやって書くんだっけ?となるのでblogに書いておく。

ポイントはHow to not use an http-router in goで紹介されているShiftPath

func ShiftPath(p string) (head, tail string) {
    p = path.Clean("/" + p)
    i := strings.Index(p[1:], "/") + 1
    if i <= 0 {
        return p[1:], "/"
    }
    return p[1:i], p[i:]
}

これを使うことでパスの最上位とそれ以降がそれぞれheadtailとして取得できるので、以下を繰り返しながらハンドリングしていく。

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var head string

    // それ以降のパスをr.URL.Pathに設定し、次のHandlerに渡す
    head, r.URL.Path = ShiftPath(r.URL.Path)

    // headを使って次のHandlerを決める
    switch head {
    case "users":
        h.users.ServeHTTP(w, r)
    default:
        http.NotFound(w, r)
    }
}

例えば次のエンドポイントを作るとしたらこんな感じ?

  • POST /api/todos
  • GET /api/todos/:id
  • POST /api/users
  • GET /api/users/:id
func ShiftPath(p string) (head, tail string) {
    p = path.Clean("/" + p)
    i := strings.Index(p[1:], "/") + 1
    if i <= 0 {
        return p[1:], "/"
    }
    return p[1:i], p[i:]
}

func NewHandler() http.Handler {
    return &rootHandler{
        api: &apiHandler{
            todos: &todosHandler{},
            users: &usersHandler{},
        },
    }
}

type rootHandler struct {
    api *apiHandler
}

func (h *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path)

    switch head {
    case "api":
        h.api.ServeHTTP(w, r)
    default:
        http.NotFound(w, r)
    }
}

type apiHandler struct {
    todos *todosHandler
    users *usersHandler
}

func (h *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path)

    switch head {
    case "todos":
        h.todos.ServeHTTP(w, r)
    case "users":
        h.users.ServeHTTP(w, r)
    default:
        http.NotFound(w, r)
    }
}

type todosHandler struct{}

func (h *todosHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path)

    switch r.Method {
    case http.MethodPost:
        // Create Todo
    case http.MethodGet:
        // Get Todo
    default:
        http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
    }
}

type usersHandler struct{}

func (h *usersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path)

    switch r.Method {
    case http.MethodPost:
        // Create User
    case http.MethodGet:
        // Get User
    default:
        http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
    }
}

うん、まあ面倒。
ちなみにtodosHandlerusersHandlerServeHTTP以降の処理は、

  1. h.handler(head).ServeHTTP(w, r)の形式で呼べるようにしても良いし、
  2. h.create(w, r)h.get(w, r, head)のようにしてしまっても良い。

※ただしheadcontextに詰めて渡すのはダメ!

1の場合はh.handlerhttp.HandlerFuncなど、ServeHTTPを実装した型を返すことになる。
もちろんstructを返しても構わないけど、How to correctly use context.Context in Go 1.7 – Jack Lindamood – Mediumにあるような割と複雑なコードになってしまうんじゃないかな。

力尽きたのでとりあえずこの辺で...