Edited at

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


はじめに

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