Go
golang

net/httpパッケージのルーター機能を拡張する(httprouterの利用)

サードパーティ製ライブラリのルーター機能を使う

net/httpパッケージでは基本的なルーティング機能のみの対応となるため、
サードパーティ製のルーター機能を実装したライブラリを使用するのがよい。

httprouterの利用

サードパーティ製ルーターの中でも最速と言われる1httprouterを利用する。

httprouterを使うには、まず下記の一行を書く。

    router := httprouter.New()

上記ではhttprouter.Routerのコンストラクタを呼んでいる。
httprouter.Router構造体は、http.Handlerインターフェースを満たすように
実装されている。

以下では、これを利用し、net/httpパッケージのみで書かれたコードを
httprouterを使うように書き換える方法を見ていく。

http.Handler

はじめに、http.HandleFuncを使用した下記のコードを
http.Handlerを使うように書き換える。

func homeHandle(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the home page!")
}

func main() {
    http.HandleFunc("/", homeHandle)
    http.ListenAndServe(":8080", nil)
}

func homeHandle() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Welcome to the home page!")
    })
}

func main() {
    http.Handle("/", homeHandle())
    http.ListenAndServe(":8080", nil)
}

上記では、まずmain関数内のhttp.HandleFunchttp.Handleに変更している。
http.Handleでは下記の通り、第二引数にhttp.Handlerを指定するよう定義されて
いるためである。

func Handle(pattern string, handler Handler) { ... }

次に、homeHandle関数をhttp.Handlerを返すように変更する。
返す値にはhttp.HandlerFuncを使用する。

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

http.HandlerFuncとは、上記の通りhttp.Handlerインターフェースを満たす
型として定義されている。

上のコードでは、func(w http.ResponseWriter, r *http.Request) { ... }
http.HandlerFunc型にキャストすることで、homeHandle関数の戻り値としている。

これで元のコードをhttp.Handlerを使うように変更できた。

httprouter.Handle

今度はhttprouter.Routerを使用するように書き換えていく。

    router := httprouter.New()
    ...
    http.ListenAndServe(":8080", router)

まず、httprouter.Routerhttp.Handlerを満たすことを利用し、
上記のようにhttp.ListenAndServeに渡すようにする。

次に、routerメソッドを使用していくのだが、ここの定義が標準パッケージとは
異なるため変更が必要となる。

下記が主要なHandleメソッドの定義となる。

func (r *Router) Handle(method, path string, handle Handle) {
    ...
}

標準パッケージとの違いは、第一引数にmethodが挿入されること、第三引数の
Handleがパッケージ独自のものであることである。

まず、第一引数には7種類のリクエストメソッドを指定するのだが、
httprouter.Routerには、このHandleメソッドを利用した7種類のメソッドが
実装されているため、そちらを使うことになる。

例えば、GETメソッドだと下記の通り定義されている。

func (r *Router) GET(path string, handle Handle) {
    r.Handle("GET", path, handle)
}

元のコードは以下のように書くことができる。

    router := httprouter.New()
    router.GET("/", httprouter.Handle型)
    http.ListenAndServe(":8080", router)

次に、router.GETメソッドの第二引数に指定するhttprouter.Handleを見ていく。
httprouter.Handleの定義は下記となる。

type Handle func(http.ResponseWriter, *http.Request, Params)

上記をみると、http.HandlerFuncに第三引数を追加したような型にも見える。
ただし、http.Handlerインターフェースは満たしていないことに注意する。

元のコードのhomeHandle関数の戻り値はhttp.Handlerであるため、何らかの方法で
httprouter.Handleに書き換える必要がある。

二通りの書き換え方法がある2ため、以下で見ていく。

homeHandle関数自体をhttprouter.Handleを返すように変更する

ひとつは、homeHandle関数自体をhttprouter.Handleを返すように変更する
方法である。

func homeHandle() httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        fmt.Fprintf(w, "Welcome to the home page!")
    }
}

func main() {
    router := httprouter.New()
    router.GET("/", homeHandle())
    http.ListenAndServe(":8080", router)
}

こちらは記述量が少ないこと、またhttprouter.Paramsがそのまま使用できるのが
利点となる。

反対に欠点としては、ひとつはhomeHandleのような関数が既に複数あった場合に、
その関数ごとに変更を入れる必要があり手間となることがあげられる。

もうひとつは、httprouter.Handleというパッケージ独自の型を各Handleごとに
利用するため、例えば他のフレームワークへの移行を考えた場合など、
パッケージ相互の互換性が低くなってしまうことがある。

httprouter.Handleを返却するwrapHandler関数を作成する

次に、もうひとつのパターンを見ていく。

http.Handlerをラップしてhttprouter.Handleを返却する
wrapHandlerのような関数を作成する方法である。

func wrapHandler(h http.Handler) httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        h.ServeHTTP(w, r)
    }
}

func homeHandle() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Welcome to the home page!")
    })
}

func main() {
    router := httprouter.New()
    router.GET("/", wrapHandler(homeHandle()))
    http.ListenAndServe(":8080", router)
}

wrapHandlerという別の関数を追加したことで一見すると記述量は多く見える。

ただし、homeHandleのような関数が既に複数あった場合には、むしろ記述量を減らせる
こととなる。各Handle関数ごとに変更を入れる必要はなく、router.GETなどに
渡す際にwrapHandlerでキャストしてやればよいのである。

また、httprouter.Paramsに格納された値が、そのままでは使用できないという点にも
注意する必要がある。

httprouter.Paramsの値を使用する場合はcontextパッケージを使うとよい
とのこと。2

使用例としては下記のようになる。

func wrapHandler(h http.Handler) httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        // cf. https://github.com/julienschmidt/httprouter/issues/198
        ctx := r.Context()
        ctx = context.WithValue(ctx, "params", ps)
        r = r.WithContext(ctx)
        h.ServeHTTP(w, r)
    }
}

func homeHandle() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Welcome to the home page!")
    })
}

func helloHandle() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ps, ok := r.Context().Value("params").(httprouter.Params)
        if !ok {
            logger.Errorf("ps is not type httprouter.Params")
        }
        fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
    })
}

func main() {
    router := httprouter.New()
    router.GET("/", wrapHandler(homeHandle()))
    router.GET("/hello/:name", wrapHandler(http.HandlerFunc(helloHandle)))
    http.ListenAndServe(":8080", router)
}

上記のままではcontext.WithValueの部分でgolintエラー3が出力される。
ここの解消方法については調べた後にまとめたいと思う。

以上のふた通りとなるが、どちらを選ぶべきかについては以下のように考える。

素早くシンプルに実装して試したい、といった場合は前者が適している。
ただ、コードの保守性を考えると、基本的には後者を選択した方が良さそうである。

例えば、ミドルウェアを追加するライブラリを使用しようとした際に、
標準パッケージに沿ったhttp.Handlerを介しての変更が比較的容易になるといった
メリットを生かすことができるためである。

まとめ

上記でhomeHandle関数をhttp.Handlerを使うように書き換えたパターンを見てきた。

しかし、この関数自体の変更せずに書き換える方法があることに、この記事を書いている
途中4で分かったので最後にメモとして残しておく。

下記では、func(http.ResponseWriter, *http.Request)型をhttp.Handler
満たすようhttp.HandlerFuncでキャストしたものを、wrapHandlerに渡すことで
httprouter.Handle型に対応させている。

func wrapHandler(h http.Handler) httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        h.ServeHTTP(w, r)
    }
}

func homeHandle(w http.ResponseWriter, r *http.Request) {   
    fmt.Fprintf(w, "Welcome to the home page!")
}

func helloHandle(w http.ResponseWriter, r *http.Request) {
    ps, ok := r.Context().Value("params").(httprouter.Params)
    if !ok {
        logger.Errorf("ps is not type httprouter.Params")
    }
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", wrapHandler(http.HandlerFunc(homeHandle)))
    router.GET("/hello/:name", wrapHandler(http.HandlerFunc(helloHandle)))
    http.ListenAndServe(":8080", router)
}

参考記事


  1. Go HTTP request router benchmark, Hacker News 

  2. How to pass the variable to 'handler'? 

  3. エラー内容はshould not use basic type string as key in context.WithValueというもの。 

  4. 正確には、http.HandlerFuncについて調べていた時。