net/httpのhandlerにdbなどの依存を持ち込む方法のメモ
昨日の記事のついでに色々goでのwebプログラミングに関する初歩的なことをメモしておこうと思った。
今回はhandlerに依存を持ち込む方法のメモ。
方法は概ね3つ。後はそれらのバリエーション。
- グローバル変数として状態を保持
- handlerを関数ではなく特定のstructのメソッドとして定義し、そのstructのフィールドに保持
- handlerではなくhandlerを返す関数として定義し、routerに登録するのは返されたクロージャにする
ちなみにわかっている人にはこのStack Overflowのページを見せれば終了という気もする。
依存とは?
そもそも此処で言う「依存」とは何のことか?ある種この場で勝手に定めた自作の用語と言っても良いかもしれないのでもう少し説明を追加する。 DI的な意味で言えば依存コンポーネント的な観点で捉えたほうがわかりやすいかもしれない。
例えばの例で言えば、タイトルなどにもあるようにDBにアクセスするためのコンポーネント。あるいは現在時刻を返す関数やロガーなんかも依存として受け取りたくなる。
これらをどうやってnet/httpのHandlerに持ち込むか?
var Handler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { // どうやって依存(e.g. db)にアクセスする? }
net/http.HandlerFuncは以下の様な型の関数になっている。正確に言えばnet/http.Handlerであっても良いのだけれどHandlerFuncだけに限定しても十分なのでここではHandlerFuncをHandlerとして考えることにする。
ここでは以下の様なstructをdbを模したものとして扱うことにする。
type Store struct { articles map[string]*Article } type Article struct { Title string `json:"title"` } func (s *Store) FindArticleByID(id string) *Article { return s.articles[id] }
1. グローバル変数として状態を保持
1ファイルで収まるようなちょっとした内部的なインフラツール等の場合はこれでも十分な事が多い。グローバル変数に状態を持たせてしまう。
// ただのグローバル変数 var store = &Store{ articles: map[string]*Article{ "1": {Title: "hello"}, }, }
ただのグローバル変数なのでhandlerの中ではそれを参照してあげれば良いだけ。
// GET /articles/{articleId} 的なものを検討 func Handler(w http.ResponseWriter, r *http.Request) { parts := strings.Split(r.URL.Path, "/") articleID := parts[len(parts)-1] ob := store.FindArticleByID(articleID) // 触れる if ob == nil { w.WriteHeader(http.StatusNotFound) return } b, err := json.Marshal(ob) if err != nil { http.Error(w, "ISE", http.StatusInternalServerError) return } w.Write(b) }
手軽ではあるがデメリットは複数の状態を持てないこと。結局の所global singletonに過ぎないので。 あと、この種の機能が一つではなく複数になっているとつらみが溢れる1。
余談: 環境変数をオプション代わりに利用
手抜きで冗長性を確保したいときなどにはこれと環境変数を見るで凌ぐ場合もある。というか多くのインフラ用のツールではこの程度で十分なことが多い。
var debugLogger = logger.NoopLogger() // 何もしないようなもの func init(){ if v, err := strconv.ParseBool(os.Getenv("DEBUG")); err == nil { debugLogger = lgger.NewLogger("debug") } }
例えばエラーメッセージを翻訳付きで扱いたいvalidatorなどでも状態を少し管理したいと思うことはある。
余談: 現在時刻の固定について
この辺は余談になるが、巷に溢れるテスト用に現在時刻を固定できる関数を作るようなコードもこれの類型。先程の話の通り時刻の固定の方法が各自様々な状況だと辛い。
var Now func()time.Time = time.Now // SetNow()みたいな関数があったり、cleanupも返えす関数になっていたりしたり、内部で真面目にlockをしていたりなど細かな違いはある
flextimeというパッケージはこれをもう少し頑張って作り込んだものの。
その他現在時刻に関する関連情報はこの辺を読むと良い
- Goで時刻をモックする · hnakamur's blog
- Goの並列テストと現在時刻に依存した実装について - pospomeのプログラミング日記
- スレッドセーフなテスト用の時間を固定するライブラリを作った - tenntenn.dev
2. handlerを関数ではなく特定のstructのメソッドとして定義
この状態をもう少しだけ範囲を限定して内に留めておく方法がある。まぁ手軽でそこそこカプセル化されていてちょっとしたmicroserviceの実装などには便利。 1.の状態で厳しくなってくるのは、環境変数で状態を指定するのが辛くなってきたとき、あるいは設定ファイルのようなものを受け取ってそれに伴って依存を初期化したいとき。
type Server struct { Store *Store } func (s *Server) GetArticle(w http.ResponseWriter, r *http.Request) { store := s.Store // フィールドなので触れる parts := strings.Split(r.URL.Path, "/") articleID := parts[len(parts)-1] ob := store.FindArticleByID(articleID) if ob == nil { w.WriteHeader(http.StatusNotFound) return } b, err := json.Marshal(ob) if err != nil { http.Error(w, "ISE", http.StatusInternalServerError) return } w.Write(b) }
今度はstructのフィールドに状態を持つことになったのでメソッドの内部ではグローバル変数のときと同様に参照できる。 この場合はhandlerを定義するパッケージを頑張って分けようとかし始めたタイミングで結構頑張ったり考えたりする事が出てくる。
(おすすめの記事があれば貼る)
3. handlerではなくhandlerを返す関数として定義
結局の所を内部的な状態をどう保持するかという話になるので別のアプローチも存在する。クロージャに閉じ込めてしまえば良い。 関数ではなく関数を返す関数を定義することになるが個人的にはこちらの方針のほうが好みではある。
func GetArticle(store *Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { parts := strings.Split(r.URL.Path, "/") articleID := parts[len(parts)-1] ob := store.FindArticleByID(articleID) if ob == nil { w.WriteHeader(http.StatusNotFound) return } b, err := json.Marshal(ob) if err != nil { http.Error(w, "ISE", http.StatusInternalServerError) return } w.Write(b) } }
例えば、以下の様な形でrouterに登録されることになる。
func Mount(r Router, store *Store) { r.Get("/articles/{articleId}", GetArticle(store)) }
基本的にはこちらのパターンかstructにフィールドをもたせるパターンで落ち着くんじゃないかとは思う。
(おすすめの記事があれば貼る2)
echoやginやそれぞれのWAF越しの利用
どのWAFを利用するにしても依存の扱いは今までの通り。以下の例は例えば2.を使った方法のechoでの実装。
(おすすめの記事があれば貼る)
認証付きのmiddleware
ここからはちょっと余談になってしまうが、認証付きのmiddlewareを考えると結構抽象の破れ的なことに気づきやすい3。 ここでのmiddlewareは以下の様なもののこと。ラップしたhttp.Handlerを返すadapter。実行時には玉ねぎ状に関数が呼ばれていく。
type Middleware = func(http.Handler) http.Handler
loginRequired的なmiddlewareを考えてみる。ここでもdbにアクセスしたい。とりあえず同様にクロージャを返すことにする。 (このときstructのfieldに状態を持つパターンでかつ肥大化してきたパッケージを分割したいみたいな気持ちが出てきたときに考えることが増える)
func LoginRequired(store *Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { k := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") _, ok := store.users[k] if !ok { w.Header().Set("WWW-Authenticate", `Bearer realm="token_required"`) w.WriteHeader(http.StatusUnauthorized) return } // TODO: bind loginUser next.ServeHTTP(w, r) }) } }
このmiddlewareをどこで提供するか?例えばhandlerの定義は一箇所にまとまっていて欲しいというとこうなる。
func GetArticle(store *Store) http.Handler { var h http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { // ... 中身は一緒 } return LoginRequired(store)(h) }
一方で、routing時に設定されて欲しい場合もある。
// chi.Routerの例 r.With(LoginRequired(store)).Get("/articles/{articleId}", GetArticle(store))
routingの方に持ってくると特定のグループに所属するendpoint全部にミドルウェアとして設置することができるのが便利。
r.Route("/myroute", func(r chi.Router) { r.With(myMiddleware).Route("/{myparam}", func(r chi.Router) { r.Get("/", getHandler) r.Put("/", putHandler) }) })
余談:特定のライフタイムを持つ依存をmiddlewareでも利用したい
これは完全に余談の余談の話になるが、dbオブジェクト的なものがライフタイムを持つようなsession的なものである必要が出てきたときに辛い話はあるかもしれない。
db, cleanup := GetDB(r)
defer cleanup()
特にこの状態で認証middlewareがそのライフタイム付きの依存を必要とした場合。 例えば、requestの持つContextにその依存をWithValueで埋め込む必要があるかもしれないし、middlewareの適用順序に慎重になる必要があるかもしれない。
ところで、それらを考慮した上でミスなく漏れなくドキュメントに認証に関する情報を記載したくなったりするがなかなか悩ましい4。
まとめ
net/httpのhandlerに依存を持ち込む方法は概ね以下3つ。
- グローバル変数として状態を保持
- handlerを関数ではなく特定のstructのメソッドとして定義し、そのstructのフィールドに保持
- handlerではなくhandler返す関数として定義し、routerに登録するのは返されたクロージャにする
dbに依存した認証middlewareのことを考えると考慮漏れに気づきやすい。
この記事も以下の記事のフォローアップみたいな感じになってしまった。
echoやGinでもGorillaでもなんでも使っても問題ありません。どれもnet/httpの兄弟です。こいつらの方が、ミドルウェアなどはたくさんリリースしているので、他の言語ユーザーには親しみやすいかもしれません。これをフレームワークと呼ぶかどうか、便利なライブラリ集と見るかはあなた次第です。