何が問題なのか
golangのミニマムREST APIのテストではテスト用と開発用のDBを切り替えて使用することがなぜかできていました。それは、同一パッケージ上ですべての関数を定義していてかつ接続するDBのパラメータをグローバル変数として定義していたため、handlerを呼び出したときのDatabaseインスタンスの値がたまたま上書きされてTEST用のDBに切り替わっていました。
しかし、すべて同一パッケージでというのは実用的ではないし、DBを選択できるという機能をAPIとして非直感的な方法で定義しておくのは気味が悪い。
変数を関数間で安全にやり取りするには
http.Handlerはfunc(ResponseWriter, *Request)のシグネチャを守る必要があるため単純に引数を一つ増やすというアプローチは取れない。
そこで、某オライリー本を参考にSyncパッケージのRWMutexとhandlerfuncをラップすることで解決してみます。
RWMuxtex
RWMutexは、Reader/Writerの相互排他ロックです。ロック状態を複数のReaderまたはひとつのWriterが持つことができます。RWMutexesは他の構造体の一部分として作成することができます。RWMutexのゼロ値は、アンロック状態のミューテックスです。
相互排他ロックというキーワードは、はじめましてですが関数間で共通の値を出し入れする場合、同時にアクセスしてしまうのは危険なので、文字通り他の関数からのアクセスをロックしてしまうことでセキュアにしているというふうに解釈します。
こちらの仕組みを使ってリクエストをキーにしてDBの情報を読み取るためのMapの値を安全に出し入れするのが目的です。
今は、こっちのリクエストの処理をしているから、他のリクエストによる値の変更は少し待ってもらうということだと思います。
実装
package main
import (
"sync"
"net/http"
)
var (
varsLock sync.RWMutex
vars map[*http.Request]map[string]interface{}
)
func OpenVars(r *http.Request) {
varsLock.Lock()
if vars == nil {
vars = map[*http.Request]map[string]interface{}{}
}
vars[r] = map[string]interface{}{}
varsLock.Unlock()
}
func CloseVars(r *http.Request) {
varsLock.Lock()
delete(vars, r)
varsLock.Unlock()
}
varsLockというsync.RWMutex型グローバル変数。マップの中にマップという構成でリクエストをキーとした中に文字列がキーで値はなんでもokなようにinterfaceで定義します。
vars[*request]["db"]
という風にハンドラー内で読み込んで(より安全な方法で)DBを指定します。
これでmainパッケージの名前空間内で自由にVarsを扱えるようになりました。
OpenVarsは、グローバル変数の初期化とリクエストに対してマップの領域を確保しています。
CloseVarsは、Mapから値を削除してメモリを不必要に消費することを防いでいます。
次により安全な方法で変数を読み込み、書き込みを行う関数です。
func GetVar(r *http.Request, key string) interface{} {
varsLock.RLock()
value := vars[r][key]
varsLock.RUnlock()
return value
}
func SetVar(r *http.Request, key string, value interface{}) {
varsLock.Lock()
vars[r][key] = value
varsLock.Unlock()
}
値読み取り、書き込みの前後にGetではvarsLock.RLock() SetではvarsLock.Lock()を読んでいるだけですが、Lockは書き込み用にロックするメソッドで、RLockは読み取り用にロックする関数のようです。https://golang.org/pkg/sync/#Mutex
次に、ハンドラーをチェインさせて直前でデータベースの値を読み取れるようにしていきましょう。
func withVars(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
OpenVars(r)
defer CloseVars(r)
fn(w, r)
}
}
func withDB(d Database, fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
Db := d
SetVar(r, "db", Db)
fn(w, r)
}
}
withVarsでOpenVarsとCloseVarsを呼び出します。次に、withDBで"db"というキーに引数で与えられたデータベースを突っ込みます。
呼び出し方はこうです、
d := fetchTestDB()
// こういう構造体を変数dに入れます。
// type Database struct{
// Service string
// User string
// Pass string
// DatabaseName string # dev or test?
// }
router.HandleFunc("/article/{id}", withVars(withDB(d, returnSingleArticle)))
ハンドラーの中で
d := GetVar(r, "db").(Database)
をして接続すれば切り替え可能です!!!