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

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

![gorilla.png](https://qiita-image-store.s3.amazonaws.com/0/221948/39938aa2-c426-3c12-a866-60c52fa49640.png)

# gorilla/mux
[gorilla/mux](https://github.com/gorilla/mux)はルーティング機能を提供します。

## インストール
```console
$ go get -u github.com/gorilla/mux
```

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

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

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

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

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

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

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

```go
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)
```

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

```go
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メソッドの種類を指定します。

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

### HTTP/HTTPS
Schemesメソッドで`http`か`https`を指定します。

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

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

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

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

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

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

```go
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}`でパスパラメータを設定します。

```go
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)`形式で保持します。

```go
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メソッドではその命名したものを指定します。

```go
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")
```

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

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

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

![スクリーンショット 2019-06-20 8.33.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221948/f6f5ce14-accd-a1a6-896e-85724756dfe2.png)
出典：https://stackphp.com/

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

```go
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
### クエリパラメータ無し版
```go
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
```

### クエリパラメータ有り版
```go
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`型)を取得する。

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

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

```go
rr := httptest.NewRecorder()
```

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

#### パラメータ無し版
`http.HandlerFunc`関数でテスト対象のハンドラーを指定します。

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

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

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

### 具体例
#### ヘルスチェックの例
テスト対象コード：

```go: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))
}
```

テストコード：

```go: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らしくテーブルドリブンに書くこともできます。

テスト対象コード：

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

テストコード：

```go: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](https://github.com/gorilla/context)はリクエストスコープで変数を管理する機能を提供します。
例えば、ヘッダに格納されたAPIキーからユーザIDを取得して、そのユーザIDをHTTPリクエスト処理内で使い回すなどといったことができます。
Set関数で値を格納して、Get関数で値を取得します。

## インストール
```console
$ go get -u github.com/gorilla/context
```

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

```go
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](https://github.com/gorilla/schema)はリクエスト変数とstruct間の変換する機能を提供します。

## インストール
```console
$ go get github.com/gorilla/schema
```

## POSTフォームからstructへ変換
最もよく使う形式です。
Decodeメソッドでstructへ変換します。

```go
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フォームへ変換します。

```go
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フォームだけでなくクエリパラメータも扱えます。

```go
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