🚀

【Golang】gorilla/muxの便利さをnet/httpと比較して理解する

に公開3

はじめに

Go言語のキャッチアップ中です。

ルーティングライブラリgorilla/muxを使ってサンプルアプリケーションを作っていたのですが、標準ライブラリでルーティングを実装する場合と比べてどの辺が便利なのかよくわからなかったため、備忘録としてまとめます。

Hello, world!

まずは最もシンプルな「Hello, world!を返す」APIを例に、標準ライブラリとgorilla/muxそれぞれのコードを見比べます。

標準ライブラリで実装

main.go
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で実装

main.go
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メソッドだけを受け付けるよう修正します。

標準ライブラリで実装

main.go
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で同じ実装をします。

main.go
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を取り出すくらいしか方法がありません。

main.go
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を使うと、パスパラメータの取得はかなり簡単になります。

main.go
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)も用意されており、パラメータがなければルートをマッチさせない設定も可能です。

標準ライブラリの場合

main.go
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の場合

main.go
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]+}")を指定すると、クエリパラメータのidageが数字以外の文字列を含む場合やパラメータがない場合は、ルートにマッチしなくなります。その結果、404エラーが返ります。
  • 必要に応じて「ハンドラで4xxを返したいか、ルートマッチのバリデーションで弾いて404で返したいか」を使い分けられるのがgorilla/muxの便利な点です。

おわりに

今後はサブルーターによるバージョン管理や、ミドルウェアとの組み合わせなども試してみたいので、その際にまたアウトプットを更新していきたいです。

また記事の内容に間違いや誤りがありましたらコメントにてご指摘いただけると幸いです。

Discussion

かずかず

ありがとうございます!
なるほど!調査不足でした!
試してみます!

Hina🐣Hina🐣

わかりやすく比較されていて参考になりました。
ありがとうございます!