ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

Gorillaのcontextとmuxを触ってみる

Gorilla, the golang web toolkit
GorillaというGolangのWeb向けツールキットについて調べ始めました。
Webアプリ用に色々なものが用意されているのですが、フレームワークではなく、あくまで必要なものを自分で取捨選択してつかう「ツールキット」です。

今回はこのGorillaの中から、contextとmuxを触ってみました。以下、公式のサンプルを参考に自分が書いてみたソースのメモ書きです。

context

context - Gorilla, the golang web toolkit

contextはリクエストの有効期間中、値を保存しておく機能です。何のこっちゃという気もしますが、ソースを見た方が手っ取り早いかと思います。

package main

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

const key1 Key = "key1"
const key2 Key = "key2"

// Key reprisents context key.
type Key string

// GetContext returns a value for this package from the request values.
func GetContext(r *http.Request, key Key) string {
	if rv := context.Get(r, key); rv != nil {
		return rv.(string)
	}
	return ""
}

// SetContext sets a value for this package in the request values.
func SetContext(r *http.Request, key Key, val string) {
	context.Set(r, key, val)
}

func contextHandler(w http.ResponseWriter, r *http.Request) {
	urlValue := r.URL.Path[1:]

	SetContext(r, key1, urlValue)
	SetContext(r, key2, time.Now().Format("2006/01/02 15:04:05"))
	// do something...
	value1 := GetContext(r, key1)
	value2 := GetContext(r, key2)

	fmt.Fprintf(w, "get key1 = %s\n", value1)
	fmt.Fprintf(w, "get key2 = %s\n", value2)
}

func main() {
	http.HandleFunc("/context/", contextHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

contextHandler内で、「http://localhost:8080/context/sample」のようなURLから「context/sample」の部分を抜き出し、キーを「key1」としてcontextに保存しています。
また現在日時を「key2」に保存しています。

「// do something...」で何らかの処理を行ったと仮定し、その後でcontextより値を取得して出力しています。

上記をビルドして実行し、curlを叩いてみると以下のように出力されます。(現在日時の部分は異なってきますが・・・)

$ curl http://localhost:8080/context/sample
get key1 = context/sample
get key2 = 2020/03/11 21:54:08

contextに設定した値が、無事取得できているようです。

mux

mux - Gorilla, the golang web toolkit

muxはザックリと書くと、URLのルーティング周りの機能を提供しています。
上記の公式が分かりやすいのですが、

  • URLパラメータを含むURLのルーティング
    • URLパラメータは正規表現で指定できる
    • サブルーティングでグループ化も可能
  • 静的ページの格納先パスを指定
  • middlewareによる共通処理の実装

などです。

これらのサンプルを書いてみました。

標準的な使い方

先に書いた

  • URLパラメータを含むURLのルーティング
    • URLパラメータは正規表現で指定できる
    • サブルーティングでグループ化も可能

を実装しています。

package main

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

	"github.com/gorilla/mux"
)

func productHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	key := vars["key"]

	fmt.Fprintf(w, "get key = %s\n", key)
}

func articlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	category := vars["category"]
	sort := vars["sort"]

	fmt.Fprintf(w, "get category = %s, sort = %s\n", category, sort)
}

func articleHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]

	fmt.Fprintf(w, "get id = %s\n", id)
}

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/products/{key}", productHandler).Methods("GET")

	s := r.PathPrefix("/articles").Subrouter()
	s.HandleFunc("/{category}/{sort:(?:asc|desc|new)}", articlesCategoryHandler).Methods("GET")
	s.HandleFunc("/{category}/{id:[0-9]+}", articleHandler).Methods("GET")

	http.Handle("/", r)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

「/products/」はURLパラメータを、「/category/」はURLパラメータの正規表現での定義、およびサブルーディングでグループ化してURLパラメータが「asc」「desc」「new」の場合と数値の場合とで別々のハンドラを指定しています。

ビルドしてcurlで実行すると以下のようになります。

$ curl http://localhost:8080/products/xxx
get key = xxx
$ curl http://localhost:8080/articles/ccc/asc
get category = ccc, sort = asc
$ curl http://localhost:8080/articles/ccc/10
get id = 10

静的ページ

cssや画像ファイルなど静的ページを格納することがあるかと思います。その静的ページの格納先の例となります。
今回は「static」というフォルダ内の「hello.html」という静的なHTMLを用意し、それを呼び出すサンプルを書いてみました。

package main

import (
	"log"
	"net/http"

	"github.com/gorilla/mux"
)

const staticDir = "/static/"

func main() {
	r := mux.NewRouter()

	r.PathPrefix(staticDir).
		Handler(http.StripPrefix(staticDir, http.FileServer(http.Dir("."+staticDir))))

	http.Handle("/", r)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

実行してcurlでこんな感じで呼び出すと、hello.htmlの内容が返却されるはずです。

$ curl http://localhost:8080/static/hello.html

middleware

各ハンドラに共通の処理を実装したい場合、middlewareという機能を使い、その中に共通処理を実装する方法があります。basic認証を共通処理として実装するサンプルを作ってみました。

package main

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

	"github.com/gorilla/mux"
)

// User ...
type User struct {
	Name     string
	Password string
}

// BasicAuthMiddleware ...
type BasicAuthMiddleware struct {
	Users []User
}

// NewBasicAuthMiddleware ...
func NewBasicAuthMiddleware() BasicAuthMiddleware {
	user1 := User{"test", "pass"}
	user2 := User{"hello", "world"}
	users := []User{user1, user2}
	return BasicAuthMiddleware{users}
}

// Authenticate ...
func (mwr *BasicAuthMiddleware) Authenticate(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		username, password, _ := r.BasicAuth()
		log.Println("Middleware Authenticated.")

		// Authenticate logic
		exist := false
		for _, u := range mwr.Users {
			if u.Name == username && u.Password == password {
				exist = true
				break
			}
		}
		if exist {
			next.ServeHTTP(w, r)
		} else {
			http.Error(w, "Forbidden", http.StatusForbidden)
		}

	})
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	timeStr := time.Now().Format("2006/01/02 15:04:05")
	fmt.Fprintf(w, "Hello, now is %s.\n", timeStr)
}

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/hello", helloHandler)

	mwr := NewBasicAuthMiddleware()
	r.Use(mwr.Authenticate)

	http.Handle("/", r)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

パスワードを平文 + べた書きで保持していますが(笑)、サンプルなので適当にやっています。本来であればデータベースなどにハッシュ化して保持するべきでしょう。。。

「helloHandler」というハンドラに対して、BasicAuthMiddleware 構造体の「Authenticate」メソッドを共通処理として事前に実行するようにしています。

実行してcurlを叩くと、ユーザとパスワードが正しいかを判定していることが分かるかと思います。

$ username=test
$ password=pass
$ curl -i --basic -u $username:$password http://localhost:8080/hello

ざっと書いたサンプルを上げてみましたが、以上です。