net/http だけでRESTなサーバーを作りたい

やりたいこと

当初の想定は以下のような感じでRESTFullなwebサーバーを標準の net/http パッケージだけで書こうと思います。

package main

import (
    "fmt"
    "handler"
    "net/http"
)

var port = 8000

type methodHandler map[string]http.Handler
func (m methodHandler) ServeHTTP(w http.ResponseWriter,r *http.Request){
    if h, ok := m[r.Method]; ok {
        h.ServeHTTP(w, r)
        return
    }
    http.Error(w, "method not allowed.", http.StatusMethodNotAllowed)
}

func Router() *http.ServeMux {
    mux := http.NewServeMux()
    // ここにHandlerを足していく...
    mux.Handle("/", methodHandler{"GET": http.HandlerFunc(handler.Index)})

    // user
    mux.Handle("/users", methodHandler{"GET": http.HandlerFunc(handler.GetUsers)})
    mux.Handle("/users/:id", methodHandler{"GET": http.HandlerFunc(handler.GetUser)})
    return mux
}

func main() {
    fmt.Println("Server Start....")
    mux := Router()
  if err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux); err != nil {
        panic(err)
    }
}

しかし userの単一リソースを取り出したいケースに置いて net/http だけでは不十分です。というのも net/http パッケージはルーティングの機能がとても貧弱で、 /users/:id 指定したhandlerに適切にroutingしてくれません。 そのため、なんとかして /users/:id でPKが id のリソースを取得する GetUser メソッドにルーティングさせる実装を独自にする必要になります。

net/http パッケージで :id を取り出して適切にhandlerにルーティングする方法については How to not use an http-router in go が参考になります。

net/httpだけで書く

ポイントは以下の2点です。

  • ShiftPath
  • リソースごとのHandlerを定義する。

ShiftPath

ポイントはこれです。実装の詳細は参考のURLにありますが以下のような ShifPath メソッドを定義します。

// src/handler/path.go

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:]
}

これはどういうことをしているかというと、メソッドのシグネチャの通り、pathからheadとtailを取り出し、tailを新しいリクエストpathとしては後続の処理に引き渡す処理です。

具体的な動作としては /users/1 のようなpathのリクエストに対して

  • haed => users
  • tail => /1 > これが後続の処理ではリクエストpathとして扱われる。

という風に分ける処理を行います。

そもそもの課題として標準の net/http パッケージでは /users/:id のようなpathのパターンを解釈できずに適切にルーティングしてくれないのがあったのでこのメソッドでリクエストのpathを解釈して適切なhandlerに登録したメソッドに振り分けます。

例えば UserHandler の場合は以下のような実装になるかと思います。

// src/handler/users.go

type UserHandler struct {}
func (h *UserHandler)ServeHTTP(w http.ResponseWriter, r *http.Request){
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path) 
  
  // この時に /users/:id のようなpathは head => users、 後続の処理で使うリクエストpath = /1 となっている。
  
  if head == "users" && r.Method == "GET"{
        GetUsers(w)
        return
    } 
  
  // もう一度ShftPathを行う。ここでは head => 1というint64の数字のはずである。
  head, r.URL.Path = ShiftPath(r.URL.Path)
    id, err := strconv.ParseInt(head, 10, 64)
    if err != nil {
        http.Error(w, fmt.Sprintf("invalid params. head: %s", head), http.StatusBadRequest)
        return
    }

    switch r.Method {
    case "GET":
        GetUser(w, id)
        return
    }

}

このような感じで再帰的に ShiftPath を繰り返していくことで適切なhandlerのメソッドにルーティングします。これ相当めんどい...。。。

リソースごとのHandlerを定義する

Handlerとして扱いたい独自構造体を定義してServeHTTPメソッドを持たせることでHandlerとしての振る舞いを持たせます。

http.Handler(pattern, handler) のpatternの部分でidをうまい具合に取り出して Handler に引き渡せればいいのだけど、普通に書いてるだけdはうまくいかないので専用の Handler を作りそのHandlerを ListenAndServeに登録するということをします。 ※ 愚直にpatternを書くようなことはしません。

// main.go

type AppHandler struct {
    RootHandler *handler.RootHandler // `/` のpath
  UserHandler *handler.UserHandler // `/users` のpath
}

func(h *AppHandler)ServeHTTP(w http.ResponseWriter, r *http.Request){
  // リクエストごとにどのリソースのHandlerにルーティングするするのかを決める。
}

AppHandler を登録したのは最終的に全てのHandlerをまとめて ListenAndServe に渡すためです。

これでサーバー部分のコードは以下のようなコードになります。

// src/main.go

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

    switch head {
    case "":
        h.RootHandler.ServeHTTP(w, r)
        return
    case "users":
        h.UserHandler.ServeHTTP(w, r)
        return
    default:
        http.Error(w, fmt.Sprintf("method not allowed request. req: %v", r.URL), http.StatusMethodNotAllowed)
        return
    }

    http.Error(w, "Not Found", http.StatusNotFound)
}

またuserリソースを取得するコードは以下のようにちょこっと変えます。main.goで UsersHandlerへの振り分けを行なっているため。

// /src/handler/users.go

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

    // main.goのAppHandlerで一度ShiftPathしているので headが空 = /users と同義
    if head == "" && r.Method == "GET"{
        GetUsers(w)
        return
    }

    id, err := strconv.ParseInt(head, 10, 64)
    if err != nil {
        http.Error(w, fmt.Sprintf("invalid params. head: %s", head), http.StatusBadRequest)
        return
    }

    switch r.Method {
    case "GET":
        GetUser(w, id)
        return
    }

}

まとめ

こんな感じで取得したいリソースのHandlerを逐一 AppHandler に登録していきますし、適切なメソッドに振り分けるまで ShiftPath を繰り返す必要があるのでとてもめんどくさいなと思います。

やはりある程度FWを使うのがいいのかなと思いますし、個人的には net/http 互換の GitHub - go-chi/chi: lightweight, idiomatic and composable router for building Go HTTP services がいいのではないかなと思っています。

余談

goのinterfaceを理解するのによく net/http のパッケージが例として使われることが多く、自分もこれで理解をしていましたが、今回車輪の再発明的な実装をしてみたことでメソッドを登録することでinterfaceとしての振る舞いをもつ、ということを再認識できました。特に ServeHTTP(w http.ResponseWriter, r *http.Request) をカスタムstructに持たせることでHandlerとして振舞うことができるというテンプレ的な表現は ServeMux の実装を見ることで理解できました。

mux := http.NewServeMux()
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux); err != nil {
  panic(err)
}

というコードに置いて ServeMuxListenAndServe に登録できるのは net/http パッケージ内で ServeHTTP が定義されていてHandlerとして振舞うことができるから、なんですね。本家のコードの中でもこのパターンは多用されておりなるほどこういうことか、と再認識できました。

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

ref. go/server.go at master · golang/go · GitHub

今回書いたコードはこちら

github.com