はじめに
Goの有名な軽量WEBツールキットであるgorillaをまとめます。
ツール毎にリポジトリが切られているため、そもそも使用しない機能はインストール自体不要ですしイメージも軽くなります。
本記事ではgorilla/muxとgorilla/contextとgorilla/schemaみを対象にします。
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メソッドでhttp
かhttps
を指定します。
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リカバリなど)をそれぞれ小さなミドルウェアとして定義し、それらを組み合わせることができます。
ミドルウェアの定義の仕方はほぼ定型です。
func sampleMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ここにミドルウェア処理を記載
next.ServeHTTP(w, r)
})
}
Useメソッドで定義したミドルウェアを使います。
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)
具体例
ヘルスチェックの例
テスト対象コード:
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))
}
テストコード:
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らしくテーブルドリブンに書くこともできます。
テスト対象コード:
func main() {
r := mux.NewRouter()
r.HandleFunc("/metrics/{type}", MetricsHandler)
log.Fatal(http.ListenAndServe("localhost:8080", r))
}
テストコード:
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
}
// 何か処理
}