サードパーティ製ライブラリのルーター機能を使う
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.HandleFunc
をhttp.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.Router
がhttp.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)
}