GoのHTTPハンドラーとconfig
どうも、gureguです。こちらはGo Advent Calendar 2の21日の記事です。
GoでHTTPサーバーを作るときの共通設定の実装について、3つのパターン(&アンチパターン)を紹介します。
問題定義
- 環境変数でSQLデータベースのDSNを設定したい
- 複数のHTTPハンドラーで1つの
*sql.Conn
を共有したい
例① グローバル変数にする(悪くない)
まず、一番シンプルなパターンです。起動時に環境変数を取得して、*sql.Conn
のグローバル変数を作ります。
var db *sql.Conn
func main() {
dsn := os.Getenv("DB_DSN")
var err error
db, err = sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/foo", fooHandler)
log.Fatal(http.ListenAndServe(":8000", nil))
}
// HTTPハンドラーは普通の関数
func fooHandler(w http.ResponseWriter, r *http.Request) {
// グローバルのdbを使います
_, err := db.Exec("...")
// ...
}
メリット
- めちゃくちゃシンプルで書きやすい
デメリット
- モック・テストしにくい
- 一部のテストだけでモックする、みたいなことが難しい
- テスト毎のデータベースの初期化のタイミングなどが難しい
- パッケージを分けにくい?
- フラットな構成を目指すなら問題ないかも
結論
個人開発やフラットさを極めたい時に使うといいでしょう。
例② structにする(ベストプラクティス)
structにconfig情報を詰めて、ハンドラーをstructのメソッドにします。
type Server struct {
db *sql.Conn
}
func (srv Server) fooHandler(w http.ResponseWriter, r *http.Request) {
// Serverのdbを使います
_, err := srv.db.Exec("...")
}
func main() {
dsn := os.Getenv("DB_DSN")
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
srv := Server{
db: db,
}
http.HandleFunc("/foo", srv.fooHandler)
log.Fatal(http.ListenAndServe(":8000", nil))
}
メリット
- グローバルじゃないのでテストしやすいです。
- テスト毎にServerを作っちゃって大丈夫です
- コードが読みやすい
- config周りは一か所だけ
デメリット
- handlerのパッケージは分けられない
- 逆にいいことかも?
結論
真面目なプロジェクトならこれがいいと思います。Server自体をhttp.Handlerにするとなお良いです。
例③ contextに入れる(アンチパターン)
皆さんはkamiを覚えていますか?
kamiの主な機能はkami.Context
によってベースとなるGod Objectなcontextです。
kami.Context
に*sql.Conn
を入れれば、全てのリクエストのhttp.Request.Context()
から取り出せます。
使い方についてはkamiのREADMEへどうぞ。
テストでモック入れたい場合はcontextの値を上書きして、使いやすい!と思いきや…
実はこれはアンチパターンです。リクエストスコープ以外のデータをcontextに入れてはいけないと、公式のドキュメントではそう書いてあります。
不思議なことに、いつのまにか標準ライブラリに同じ機能が入りました。Server.BaseContextをいじると、kamiと同じことが出来ます。しかしBaseContextは関数なので、必ずしも悪用な用途にはならないかと思います。
メリット
- パッケージが分けやすい?
デメリット
- 公式認定のアンチパターン
- contextにどこから何が入っているかが分かりにくい
結論
やめときましょう。
おまけ: os.Getenvを使うタイミング
これは好みかもしれませんが、個人的には、mainパッケージ[1]以外でos.Getenvなどを呼びたくないです。
全てのconfigの初期化は同じ場所でやりたい。複数のパッケージがos.Getenvを呼んでいると、どこが何に依存しているかが分かりにくいです。
おわりに
皆さんはどういうconfigが好きですか?他のやり方があったらぜひコメントしてください。
私はシンプルなプロトタイプなどではグローバル変数を使って、真面目な仕事はstructにしています。
-
config専用のパッケージでもOKです ↩︎
Discussion