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:] }
これを使うことでパスの最上位とそれ以降がそれぞれhead
、tail
として取得できるので、以下を繰り返しながらハンドリングしていく。
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) } }
例えば次のエンドポイントを作るとしたらこんな感じ?
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) } }
うん、まあ面倒。
ちなみにtodosHandler
やusersHandler
のServeHTTP
以降の処理は、
h.handler(head).ServeHTTP(w, r)
の形式で呼べるようにしても良いし、h.create(w, r)
やh.get(w, r, head)
のようにしてしまっても良い。
※ただしhead
をcontext
に詰めて渡すのはダメ!
1の場合はh.handler
がhttp.HandlerFunc
など、ServeHTTP
を実装した型を返すことになる。
もちろんstruct
を返しても構わないけど、How to correctly use context.Context in Go 1.7 – Jack Lindamood – Mediumにあるような割と複雑なコードになってしまうんじゃないかな。
力尽きたのでとりあえずこの辺で...