🛣️

Go 1.22のEnhanced ServeMuxに合わせて設計されたルーティングライブラリmichi

2024/03/23に公開

https://github.com/go-michi/michi

michiはGo 1.22のEnhanced ServeMuxに合わせて設計された設計されたルーティングライブラリです。

以下で試すことができます。
https://go.dev/play/p/wBWzrwkVD5j

機能

  • 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機能を利用します。

michi

chi

使い方

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のドキュメントを読んでください。
https://pkg.go.dev/net/http#ServeMux

michiの詳細な使い方はGoDocのExampleを確認してください。
https://pkg.go.dev/github.com/go-michi/michi

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から可能になりました。
https://github.com/go-chi/chi/releases/tag/v5.0.12
https://github.com/go-chi/chi/pull/897

// 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から可能になりました。
https://github.com/go-chi/chi/releases/tag/v5.0.12
https://github.com/go-chi/chi/pull/901

ルーティング判定条件

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/にマッチしません。
  • Go 1.22移行で/a/{$}にHandlerを登録して、/a/bにリクエストを送った場合
    • http.ServeMuxとmichiはchiと同じように404 Not Foundを返します。
      • 完全一致で/a/bは/a/にマッチしません。

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