【Golang】gorilla/muxの便利さをnet/httpと比較して理解する
はじめに
Go言語のキャッチアップ中です。
ルーティングライブラリgorilla/muxを使ってサンプルアプリケーションを作っていたのですが、標準ライブラリでルーティングを実装する場合と比べてどの辺が便利なのかよくわからなかったため、備忘録としてまとめます。
Hello, world!
まずは最もシンプルな「Hello, world!
を返す」APIを例に、標準ライブラリとgorilla/muxそれぞれのコードを見比べます。
標準ライブラリで実装
package main
import (
"io"
"log"
"net/http"
)
func HelloHandler(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "Hello, world!\n")
}
func main() {
http.HandleFunc("/hello", HelloHandler)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
- 標準ライブラリnet/httpだけを使う場合は、
http.HandleFunc
でパスとハンドラを紐づける形が基本的な書き方です。 - 上の例では、どんなリクエストメソッド(GET/POST/PUT/DELETEなど)でアクセスしても同じ
HelloHandler
が呼ばれます。
gorilla/muxで実装
package main
import (
"io"
"log"
"net/http"
"github.com/gorilla/mux"
)
func HelloHandler(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "Hello, world!\n")
}
func main() {
// ルータを作成
r := mux.NewRouter()
// http.HandleFunc から変更
r.HandleFunc("/hello", HelloHandler)
// ListenAndServe の第2引数に gorilla/mux のルータ r を渡す
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatal(err)
}
}
- gorilla/muxでは
mux.NewRouter()
を使って任意のルータを生成し、それにパスやメソッドを紐づけます。 - この段階ではまだルーティングライブラリを使うメリットはあまり感じられないかもしれません。単に
Hello, world!
を返すだけなら標準ライブラリで十分という印象です。
HTTPメソッドの種類を制限する
次に「あるパスに対してGETメソッドだけ許可したい」というケースを考えます。
先ほどのHello, world!
を返すAPIがGETメソッドだけを受け付けるよう修正します。
標準ライブラリで実装
package main
import (
"io"
"log"
"net/http"
)
func HelloHandler(w http.ResponseWriter, req *http.Request) {
// GET メソッド以外は 405 (Method Not Allowed) を返す
if req.Method == http.MethodGet {
io.WriteString(w, "Hello, world!\n")
} else {
http.Error(w, "HTTPメソッドが不正です。", http.StatusMethodNotAllowed)
}
}
func main() {
http.HandleFunc("/hello", HelloHandler)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
-
if req.Method == http.MethodGet { ... }
のようにハンドラ内で条件分岐を行います。 - メソッドが複数になると分岐が増えて煩雑になってしまいます。
gorilla/muxで実装
gorilla/muxで同じ実装をします。
package main
import (
"io"
"log"
"net/http"
"github.com/gorilla/mux"
)
func HelloHandler(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "Hello, world!\n")
}
func main() {
r := mux.NewRouter()
// Methods で指定したメソッド以外の場合は405エラーが返される
r.HandleFunc("/hello", HelloHandler).Methods(http.MethodGet)
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatal(err)
}
}
- gorilla/muxでは、ルータの設定時に
.Methods(http.MethodGet)
と書くだけで、GETメソッド以外は自動的に405エラーを返します。 - ハンドラ内の条件分岐は不要になるため、コードの記述量を減らせますし可読性も高いです。
パスパラメータの扱い
続いてURLのパスにユーザーIDなどの値を埋め込んで受け取る場合を考えます。
たとえば/users/123
のようなパスでIDを指定して、そのユーザーを取得するAPIです。
標準ライブラリの場合
標準ライブラリで/users/123
へリクエストを受け取った場合、正規表現や"/"
区切りの文字列分割などで123
というIDを取り出すくらいしか方法がありません。
package main
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
)
func UserHandler(w http.ResponseWriter, req *http.Request) {
// GET メソッドのみ許可
if req.Method == http.MethodGet {
// "/users/123" を "/" で分割して ["", "users", "123"] にする
parts := strings.Split(req.URL.Path, "/")
if len(parts) < 3 {
http.Error(w, "ユーザーIDが指定されていません。", http.StatusBadRequest)
return
}
// 0番目は空文字, 1番目は "users", 2番目が "123"
userIDStr := parts[2]
// 数値変換
userID, err := strconv.Atoi(userIDStr)
if err != nil {
http.Error(w, "数値以外のIDは無効です", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "User ID: %d\n", userID)
} else {
http.Error(w, "HTTPメソッドが不正です。", http.StatusMethodNotAllowed)
}
}
func main() {
http.HandleFunc("/users/", UserHandler)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
- パスの情報は
req.URL.Path
が文字列として保持するため、それをstrings.Split
などで分解してパラメータを取り出しています。その他に正規表現で抽出することもできそうです。 - 文字列の処理が中心なので、パラメータが増えるほどコードも増えてしまいます。
gorilla/muxの場合
gorilla/muxを使うと、パスパラメータの取得はかなり簡単になります。
package main
import (
"fmt"
"log"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
func UserHandler(w http.ResponseWriter, req *http.Request) {
// mux.Vars(req) で {id} の値を取得できる
userIDStr := mux.Vars(req)["id"]
// 数値変換
userID, err := strconv.Atoi(userIDStr)
if err != nil {
http.Error(w, "数値以外のIDは無効です", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "User ID: %d\n", userID)
}
func main() {
r := mux.NewRouter()
// こんなふうにパス部分に {id} を埋め込む
r.HandleFunc("/users/{id}", UserHandler).Methods(http.MethodGet)
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatal(err)
}
}
- gorilla/muxでは、
/users/{id}
のようにプレースホルダを指定し、ハンドラ側でmux.Vars(req)["id"]
と書くだけでパスパラメータの値を取得できます。 - 手動で文字列の分割や正規表現を使わなくてよいため、可読性・保守性が向上します。
クエリパラメータの扱い
URLの末尾に?id=123&age=20
のように付与されるクエリパラメータの取得方法は、標準ライブラリでもgorilla/muxでも大きくは変わりません。
ただしgorilla/muxにはクエリパラメータをバリデーションする仕組み(例: Queries
)も用意されており、パラメータがなければルートをマッチさせない設定も可能です。
標準ライブラリの場合
package main
import (
"fmt"
"log"
"net/http"
"strconv"
)
func UserHandler(w http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodGet {
// req.URL.Query() を呼び出すとクエリの map[string][]string が取れる
query := req.URL.Query()
idStr := query.Get("id")
ageStr := query.Get("age")
// 値が存在しない場合は空文字列が返ってくる
if idStr == "" {
fmt.Fprintln(w, "クエリパラメータ id が指定されていません")
return
}
if ageStr == "" {
fmt.Fprintln(w, "クエリパラメータ age が指定されていません")
return
}
// 数値変換
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "id は数値で指定してください", http.StatusBadRequest)
return
}
age, err := strconv.Atoi(ageStr)
if err != nil {
http.Error(w, "age は数値で指定してください", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "UserID: %d, Age: %d\n", id, age)
} else {
http.Error(w, "HTTPメソッドが不正です。", http.StatusMethodNotAllowed)
}
}
func main() {
http.HandleFunc("/users", UserHandler)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
gorilla/muxの場合
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func UserHandler(w http.ResponseWriter, req *http.Request) {
query := req.URL.Query()
id := query.Get("id")
age := query.Get("age")
fmt.Fprintf(w, "UserID: %s, Age: %s\n", id, age)
}
func main() {
r := mux.NewRouter()
// Queries("id", "{id:[0-9]+}", "age", "{age:[0-9]+}")
// と書くと、クエリの形式が合わない場合に 404 エラーになる
r.HandleFunc("/users", UserHandler).Methods(http.MethodGet).Queries("id", "{id:[0-9]+}", "age", "{age:[0-9]+}")
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatal(err)
}
}
-
.Queries("id", "{id:[0-9]+}", "age", "{age:[0-9]+}")
を指定すると、クエリパラメータのid
やage
が数字以外の文字列を含む場合やパラメータがない場合は、ルートにマッチしなくなります。その結果、404エラーが返ります。 - 必要に応じて「ハンドラで4xxを返したいか、ルートマッチのバリデーションで弾いて404で返したいか」を使い分けられるのがgorilla/muxの便利な点です。
おわりに
今後はサブルーターによるバージョン管理や、ミドルウェアとの組み合わせなども試してみたいので、その際にまたアウトプットを更新していきたいです。
また記事の内容に間違いや誤りがありましたらコメントにてご指摘いただけると幸いです。
Discussion
こんにちは!
Go1.22からは標準ライブラリでもメソッドごとのルーティングやパスパラメータは対応されましたので、そちらを試されても良いかもしれませんね(╹◡╹)
ありがとうございます!
なるほど!調査不足でした!
試してみます!
わかりやすく比較されていて参考になりました。
ありがとうございます!