Help us understand the problem. What is going on with this article?

【Go×WAF】うほうほ!!gorillaを分かりやすくまとめてみた【mux/context/schema】

More than 1 year has passed since last update.

はじめに

Goの有名な軽量WEBツールキットであるgorillaをまとめます。
ツール毎にリポジトリが切られているため、そもそも使用しない機能はインストール自体不要ですしイメージも軽くなります。
本記事ではgorilla/muxとgorilla/contextとgorilla/schemaみを対象にします。

gorilla.png

gorilla/mux

gorilla/muxはルーティング機能を提供します。

インストール

$ go get -u github.com/gorilla/mux

機能

ルーティング

HandleFuncメソッドでパスとハンドラーの紐付けをします。
また、はじめにNewRouter関数で*Router型の変数を作る必要があります。
gorilla/muxでは*Router型変数が持つメソッドを利用することがメインになります。

r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)
r.HandleFunc("/products", ProductsHandler)
r.HandleFunc("/articles", ArticlesHandler)
http.Handle("/", r)

ドメイン名設定

Hostメソッドでドメイン名を設定します。

r := mux.NewRouter()
// "www.example.com"をドメイン名設定
r.Host("www.example.com")
// 正規化を使って柔軟にドメイン名設定
r.Host("{subdomain:[a-z]+}.example.com")

パスプレフィックス

PathPrefixメソッドで共通的に使用するパスのプレフィックスを設定します。

r := mux.NewRouter()
r.PathPrefix("/v1/")

サブルーチン

Subrouterメソッドでドメイン名設定を1回だけして、それを使い回せます。

r := mux.NewRouter()
s := r.Host("www.example.com").Subrouter()

s.HandleFunc("/products/", ProductsHandler)

s.HandleFunc("/products/{key}", ProductHandler)

s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)

また、ドメイン名だけでなくパスプレフィックスにも有効です。

r := mux.NewRouter()
s := r.PathPrefix("/products").Subrouter()

// "/products/"
s.HandleFunc("/", ProductsHandler)

// "/products/{key}/"
s.HandleFunc("/{key}/", ProductHandler)

// "/products/{key}/details"
s.HandleFunc("/{key}/details", ProductDetailsHandler)

HTTPメソッド

MethodsメソッドでHTTPメソッドの種類を指定します。

r := mux.NewRouter()
r.Methods("GET", "POST")

HTTP/HTTPS

Schemesメソッドでhttphttpsを指定します。

r := mux.NewRouter()
r.Schemes("https")

HTTPヘッダ

HeadersメソッドでHTTPヘッダを指定します。

r := mux.NewRouter()
r.Headers("X-Requested-With", "XMLHttpRequest")

クエリパラメータ

Queriesメソッドでクエリパラメータを設定します。

r := mux.NewRouter()
r.HandleFunc("/authors", handler).Queries("surname", "{surname}")

標準httpパッケージのQuery関数で取得します。

func AuthorsHandler(w http.ResponseWriter, r *http.Request) {
    var name string
    err := decoder.Decode(&name, r.URL.Query())
    if err != nil {
        ResponseBadRequest(w, err.Error())
        return
    }
}

パスパラメータ

{name}あるいは{name:pattern}でパスパラメータを設定します。

r := mux.NewRouter()
r.HandleFunc("/products/{key}", ProductHandler) //パスパラメータ設定
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) //パスパラメータ設定
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) //パスパラメータ設定

Vars関数でパラメータの値を取得します。
Vars関数はmap[string]string型を返すため、パラメータをkey(string)/value(string)形式で保持します。

func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r) //パスパラメータ取得
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "Category: %v\n", vars["category"])
}

URLビルド

NameメソッドでURLに命名します。
Getメソッドではその命名したものを指定します。

r := mux.NewRouter()
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
  Name("article")

# `/articles/technology/42`にGETリクエスト
url, _ := r.Get("article").URL("category", "technology", "id", "42")

これまで紹介した複数の機能を組み合わせることもできます。

r.HandleFunc("/products", ProductsHandler).
  Host("www.example.com").
  Methods("GET").
  Schemes("http")

ミドルウェア

HTTPリクエスト処理に関する共通的な前後処理(DB、ログ、認証、panicリカバリなど)をそれぞれ小さなミドルウェアとして定義し、それらを組み合わせることができます。

スクリーンショット 2019-06-20 8.33.27.png
出典:https://stackphp.com/

ミドルウェアの定義の仕方はほぼ定型です。

func sampleMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // ここにミドルウェア処理を記載

        next.ServeHTTP(w, r)
    })
}

Useメソッドで定義したミドルウェアを使います。

go
r := mux.NewRouter()
r.HandleFunc("/", handler)
// ミドルウェアの適用
r.Use(sampleMiddleware)

少し本格的な認証ミドルウェアの例:

// Define our struct
type authenticationMiddleware struct {
    tokenUsers map[string]string
}

// Initialize it somewhere
func (amw *authenticationMiddleware) Populate() {
    amw.tokenUsers["00000000"] = "user0"
    amw.tokenUsers["aaaaaaaa"] = "userA"
    amw.tokenUsers["05f717e5"] = "randomUser"
    amw.tokenUsers["deadbeef"] = "user0"
}

// Middleware function, which will be called for each request
func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("X-Session-Token")

        if user, found := amw.tokenUsers[token]; found {
            // We found the token in our map
            log.Printf("Authenticated user %s\n", user)
            // Pass down the request to the next middleware (or final handler)
            next.ServeHTTP(w, r)
        } else {
            // Write an error and stop the handler chain
            http.Error(w, "Forbidden", http.StatusForbidden)
        }
    })
}

r := mux.NewRouter()
r.HandleFunc("/", handler)

amw := authenticationMiddleware{}
amw.Populate()

r.Use(amw.Middleware)

Full examples

クエリパラメータ無し版

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/gorilla/mux"
)

// ハンドラー。処理を記述する。
func sampleHandler1(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello sample")
}

func main() {
    // ルーティング設定
    r := mux.NewRouter()
    r.HandleFunc("/sample1", sampleHandler1)

    // サーバ設定
    srv := &http.Server{
        Handler:      r,
        Addr:         "127.0.0.1:8000",
        WriteTimeout: 15 * time.Second,
        ReadTimeout:  15 * time.Second,
    }

    // 起動
    log.Fatal(srv.ListenAndServe())
}
実行結果
$ curl http://localhost:8000/sample1
hello sample

クエリパラメータ有り版

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/gorilla/mux"
)

func sampleHandler2(w http.ResponseWriter, r *http.Request) {
    // クエリパラメータの取得
    vars := mux.Vars(r)
    fmt.Fprintf(w, vars["name"])
}

func main() {
    r := mux.NewRouter()
    // クエリパラメータ指定
    r.HandleFunc("/sample2/{name}", sampleHandler2)

    srv := &http.Server{
        Handler:      r,
        Addr:         "127.0.0.1:8000",
        WriteTimeout: 15 * time.Second,
        ReadTimeout:  15 * time.Second,
    }

    log.Fatal(srv.ListenAndServe())
}

実行結果
$ curl http://localhost:8000/sample2/gold-kou
gold-kou

テスト

gorilla/muxで特別なテスト機能がふんだんに用意されていたりはしません。
Goの標準機能のみでシンプルにテストを書けます。

リクエスト&レスポンス

リクエスト準備

http.NewRequest関数の第一引数にHTTPメソッド、第二引数にURL、第三引数にパラメータ(無い場合はnil)を渡し、戻り値(*Request型)を取得する。

req, err := http.NewRequest("GET", "/health", nil)

レスポンス準備

httptest.NewRecorder関数の戻り値(*ResponseRecorder型)を取得する。
ResponseRecorderはフィールドにBodyやCodeを持つ構造体である。

rr := httptest.NewRecorder()

リクエスト実行

ServeHTTPでHTTPリクエストを実行します。

パラメータ無し版

http.HandlerFunc関数でテスト対象のハンドラーを指定します。

handler := http.HandlerFunc(HealthCheckHandler)
handler.ServeHTTP(rr, req)

パラメータ有り版

パラメータを渡す場合は、httpパッケージでなくmux.NewRouterメソッドの返り値(*Router型)を使います。

router := mux.NewRouter()
router.HandleFunc("/metrics/{type}", MetricsHandler)
router.ServeHTTP(rr, req)

具体例

ヘルスチェックの例

テスト対象コード:

endpoints.go
package main

// 単なるヘルスチェックAPI
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    io.WriteString(w, `{"alive": true}`)
}

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/health", HealthCheckHandler)
    log.Fatal(http.ListenAndServe("localhost:8080", r))
}

テストコード:

endpoints_test.go
package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHealthCheckHandler(t *testing.T) {
    // リクエスト準備
    req, err := http.NewRequest("GET", "/health", nil)
    if err != nil {
        t.Fatal(err)
    }

    // レスポンス準備
    rr := httptest.NewRecorder()

    // リクエスト実行
    handler := http.HandlerFunc(HealthCheckHandler)
    handler.ServeHTTP(rr, req)

    // レスポンスのステータスコードが期待通りか確認
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }

    // レスポンスのボディが期待通りか確認
    expected := `{"alive": true}`
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v",
            rr.Body.String(), expected)
    }
}

テーブルドリブンに書く例

もちろんGoらしくテーブルドリブンに書くこともできます。

テスト対象コード:

endpoints.go
func main() {
    r := mux.NewRouter()
    r.HandleFunc("/metrics/{type}", MetricsHandler)
    log.Fatal(http.ListenAndServe("localhost:8080", r))
}

テストコード:

endpoints_test.go
func TestMetricsHandler(t *testing.T) {
    tt := []struct{
        routeVariable string
        shouldPass bool
    }{
        {"goroutines", true},
        {"heap", true},
        {"counters", true},
        {"queries", true},
        {"adhadaeqm3k", false},
    }

    for _, tc := range tt {
        path := fmt.Sprintf("/metrics/%s", tc.routeVariable)
        req, err := http.NewRequest("GET", path, nil) // pathにパラメータ情報があるので第三引数はnil
        if err != nil {
            t.Fatal(err)
        }

        rr := httptest.NewRecorder()


         router := mux.NewRouter()
        router.HandleFunc("/metrics/{type}", MetricsHandler)
        router.ServeHTTP(rr, req)

        // In this case, our MetricsHandler returns a non-200 response
        // for a route variable it doesn't know about.
        if rr.Code == http.StatusOK && !tc.shouldPass {
            t.Errorf("handler should have failed on routeVariable %s: got %v want %v",
                tc.routeVariable, rr.Code, http.StatusOK)
        }
    }
}

gorilla/context

gorilla/contextはリクエストスコープで変数を管理する機能を提供します。
例えば、ヘッダに格納されたAPIキーからユーザIDを取得して、そのユーザIDをHTTPリクエスト処理内で使い回すなどといったことができます。
Set関数で値を格納して、Get関数で値を取得します。

インストール

$ go get -u github.com/gorilla/context

具体例

変数aをリクエストスコープで扱う例です。

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/gorilla/context"
    "github.com/gorilla/mux"
)

// contextで扱う変数
var a string

func setA(w http.ResponseWriter, r *http.Request) {
    // リクエストスコープに変数をセット
    context.Set(r, a, "foo")
}

func sampleContextHandler(w http.ResponseWriter, r *http.Request) {
    setA(w, r)
    // リクエストスコープの変数の値を取得
    val := context.Get(r, a)
    fmt.Fprintf(w, val.(string))
}

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/sampleContext", sampleContextHandler)

    srv := &http.Server{
        Handler:      r,
        Addr:         "127.0.0.1:8000",
        WriteTimeout: 15 * time.Second,
        ReadTimeout:  15 * time.Second,
    }

    log.Fatal(srv.ListenAndServe())
}
実行結果
$ curl http://localhost:8000/sampleContext
foo

gorilla/schema

gorilla/schemaはリクエスト変数とstruct間の変換する機能を提供します。

インストール

$ go get github.com/gorilla/schema

POSTフォームからstructへ変換

最もよく使う形式です。
Decodeメソッドでstructへ変換します。

var decoder = schema.NewDecoder()

type Person struct {
    Name  string
    Phone string
}

func MyHandler(w http.ResponseWriter, r *http.Request) {
    err := r.ParseForm()
    if err != nil {
        // Handle error
    }

    var person Person

    // POSTフォームで送信された変数をstructに変換する
    err = decoder.Decode(&person, r.PostForm)
    if err != nil {
        // Handle error
    }

    // 何か処理
}

structからPOSTフォームへ変換

EncodeメソッドでPOSTフォームへ変換します。

var encoder = schema.NewEncoder()

type Person struct {
    Name  string `schema:"name,required"`  // custom name, must be supplied
    Phone string `schema:"phone"`          // custom name
    Admin bool   `schema:"-"`              // this field is never set
}

func MyHttpRequest() {
    person := Person{"Jane Doe", "555-5555"}
    form := url.Values{}

    // structをPOSTフォームに変換
    err := encoder.Encode(person, form)

    if err != nil {
        // Handle error
    }

    // POSTフォーム送信
    client := new(http.Client)
    res, err := client.PostForm("http://my-api.test", form)
}

クエリパラメータからstructへ変換

POSTフォームだけでなくクエリパラメータも扱えます。

var decoder = schema.NewDecoder()

type Person struct {
    Name  string
    Phone string
}

func MyHandler(w http.ResponseWriter, r *http.Request) {
    var person Person

    // クエリパラメータをstructに変換する
    err := decoder.Decode(&person, r.URL.Query())
    if err != nil {
        // Handle error
    }

    // 何か処理
}

参考サイト

http://www.gorillatoolkit.org

gold-kou
NTT米屋➡︎ZOZOテクノロジーズ スクラムマスター兼バックエンドエンジニアです。 GoとかgRPCとかOpenAPIとか。 外部記憶として学んだことを記事として残しています。 私の記事は個人的なものであり、会社を代表するものではございません。
zozotech
70億人のファッションを技術の力で変えていく
https://tech.zozo.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away