Go 1.22のEnhanced ServeMuxに合わせて設計されたルーティングライブラリmichi
michiはGo 1.22のEnhanced ServeMuxに合わせて設計された設計されたルーティングライブラリです。
以下で試すことができます。
機能
- net/httpと100%の互換性 - http.ServerMux, http.Handler and http.HandlerFunc
- Enhanced http.ServeMux - Go 1.22からHTTP Methodの指定とPathValueの取得ができます。
- chiのようなインターフェース - Route, Group, With and chi middlewares
- 外部パッケージへの依存はありません - 標準packageのみを利用しています
- 軽量 - コードは160行以下です。 michiはHandlerのセットアップだけをサポートしており、ルーティング機能はhttp.ServeMuxへ委譲します。
- パフォーマンス - 高速です。michiとhttp.ServeMuxは同等のパフォーマンスです。
背景
Go1.21以前のnet/http packageでは、HTTP Methodの指定とURLに含まれるPath Valueの取得をする方法がありませんでした。そのため以下のように、これらの処理をHandler内部で実装する必要があります。
mux := http.NewServeMux()
// Handler設定時にHTTP Methodの指定やPathValueの指定はできない
mux.Handle("/user/", func(w http.ResponseWriter, r *http.Request) {
// HTTP Methodのフィルタリングは、Handler内部で実装する必要がある
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// PathValueの実装は、Handler内部で実装する必要がある
// GET /user/12345 -> Hello 12345
values := strings.Split(r.URL.Path, "/") // 正確な実装ではない簡易的なもの
w.Write([]byte("Hello " + values[len(values)-1]))
})
そのため今までは代わりにchiやgorilla/muxなどのルーティングライブラリ、echoやginなどのWeb Frameworkがその機能を担ってきました。
// chi
r := chi.NewRouter()
// HTTP MethodとPathValueはHandler設定時に指定可能
r.Get("/user/{id}", func(w http.ResponseWriter, r *http.Request) {
// Handler内部ではHTTP MethodとPathValueの実装は不要
// GET /user/12345 -> Hello 12345
w.Write([]byte("Hello " + chi.URLParam(r, "id")))
})
この問題を解決するためGo 1.22では、HTTP Methodの指定とPathValueの取得が可能になりました。
mux := http.NewServeMux()
mux.HandleFunc("GET /user/{id}", func(w http.ResponseWriter, r *http.Request) {
// Handler内部ではHTTP MethodとPathValueの実装は不要
// GET /user/12345 -> Hello 12345
w.Write([]byte("Hello " + r.PathValue("id")))
})
このようにhttp.ServeMuxでいくつかの問題が解決したためchiからhttp.ServeMuxへの移行を検討可能になりました。
しかしchiにはHandlerやMiddlewareの設定を便利するインターフェースがありますが、これらの機能はhttp.ServeMuxにはありません。
r := chi.NewRouter()
// Useメソッドによるmiddlewareの設定
r.Use(middleware.Logger)
// Routeメソッドによるルーティングのネスト
r.Route("/user", func(r chi.Router) {
// /user以下にだけ影響するMiddlewareの設定
r.Use(middleware.UserLogger)
r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello " + r.PathValue("id")))
})
}
そこでchiのHandlerやMiddlewareを設定するインターフェースを持ち、ルーティング機能はhttp.ServeMuxに委譲する michi
を開発しました。
michiにより標準packageのhttp.ServeMuxが持っているルーティング機能とchiの利便性を両立することが可能になります。
以下はchiとmichiの違いを表した図です。chiは独自のRouting機能を持ちますがmichiはhttp.ServeMuxのRouting機能を利用します。
使い方
go get -u github.com/go-michi/michi
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-michi/michi"
)
func main() {
rt := michi.NewRouter()
// chiのインターフェースと互換性があるためchiに依存していないchiのMiddlewareが利用可能です
rt.Use(middleware.Logger)
rt.Route("/user", func(rt michi.Router) {
rt.HandleFunc("GET /{id}", func(w http.ResponseWriter, r *http.Request) {
// GET /user/12345 -> Hello 12345
w.Write([]byte("Hello " + r.PathValue("id")))
})
}
http.ListenAndServe(":3000", rt)
}
michiを使う前に、必ずhttp.ServeMuxのドキュメントを読んでください。
michiの詳細な使い方はGoDocのExampleを確認してください。
http.ServeMux(michi)を使う上での注意
chiとmichiには以下のような違いがあります。
Methodの指定方法
http.ServeMuxとmichiはHTTP Method別のハンドラ登録メソッドがありません。代わりにPathの先頭にHTTP Methodを指定します。
// chi
r.Get("/user{id}", userHandler)
// michi
r.HandleFunc("GET /user/{id}", userHandler)
chiでもv5.0.12から可能になりました。
// chi
r.HandleFunc("GET /user/{id}", userHandler)
Path Valueの取得
http.ServeMuxとmichiはPath Valueをhttp.RequestのPathValueメソッドで取得します。
// chi
id := chi.URLParam(r, "id")
// michi
id := r.PathValue("id")
chiでもv5.0.12から可能になりました。
ルーティング判定条件
http.ServeMuxとmichiは前方一致で、chiは完全一致です。しかしGo1.22からはPathの末尾に{$}を指定にすることで完全一致にすることができます。
- /a/にHandlerを登録して、/a/bにリクエストを送った場合
- http.ServeMuxとmichiは/a/のHandlerを実行します。
- 前方一致で/a/bは/a/にマッチします。
- chiは404 Not Foundを返します。
- 完全一致で/a/bは/a/にマッチしません。
- http.ServeMuxとmichiは/a/のHandlerを実行します。
- Go 1.22移行で/a/{$}にHandlerを登録して、/a/bにリクエストを送った場合
- http.ServeMuxとmichiはchiと同じように404 Not Foundを返します。
- 完全一致で/a/bは/a/にマッチしません。
- http.ServeMuxとmichiはchiと同じように404 Not Foundを返します。
Valueのパターンマッチ
chiは以下のようにパターンを指定できます。http.ServeMuxとmichiではできません。
- 正規表現
/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}
/date/2017/04/01
- 部分一致
/user-{userID}
/user-12345
- 複数のValue
/{yyyy}-{mm}-{dd}
/2024-10-10
Sub RouterのMount
http.ServeMuxとmichiはSub Routerの登録にHandleメソッドを使います。chiはMountメソッドを使います。
またHandleメソッドとMountメソッドにはPathの指定方法に違いがあります。
- http.ServeMuxのHandleメソッドは、親のPathを省略できません。
aRouter := http.NewServeMux()
// /a/を省略できない
aRouter.HandleFunc("/a/hello", handler("hello"))
rt := http.NewServeMux()
rt.HandleFunc("/a/", aRouter)
- chiのMountwメソッドは、親のpathを省略できます。
aRouter := chi.NewRouter()
// /a/を省略できる。
aRouter.HandleFunc("/hello", handler("hello"))
rt := chi.NewRouter()
rt.Mount("/a", aRouter)
- michiのHandleメソッドはhttp.ServeMuxと同じで、親のPathを省略できません。
aRouter := michi.NewRouter()
// /a/を省略できない
aRouter.HandleFunc("/a/hello", handler("hello"))
rt := michi.NewRouter()
rt.Handle("/a/", aRouter)
- Routeを使うことでシンプルに書くことは可能です。
// michi
func main() {
r := michi.NewRouter()
// can't omit /a/ path
r.Route("/a", func(r michi.Router) {
r.HandleFunc("/hello", handler("hello"))
})
}
まとめ
Go 1.22以降のEnhanced http.ServeMuxの利用とchiの利便性を両立するmichiを紹介しました。
私は標準packageを可能な限り利用することが好きであり、GraphQLということもありValueのパターンマッチの機能をルータで利用することがありません。そのためmichiを使いたいと考えています。しかし依然としてchiは利便性で優っている点があり、必ずしも移行が必要とは言えません。依存性や利便性を元に判断をしてご利用ください。
Discussion