クリスマス期間になると喜ぶ人もいれば悲しむ人もいる。そんな極端な季節の中で、自分が作ったguregu/kami
というWAFについて真面目に考えた。context
の正しい使い方や、kamiのAPIで後悔していることを晒そう。
kamiを作ったきっかけ
まずはkamiの歴史について簡単に説明する。GunosyでアプリのAPIサーバーをRailsからGoに少しずつ書き直していたが、モノリシックだったmodelsとhandlersパッケージが大きくなりすぎて分かりにくくなってしまった。大きいパッケージを複数の小さなパッケージに分けようと思った。しかしユーザーのセッション情報などは様々なパッケージにどう共有したらいいでしょう?
当時使っていたGojiというWAFでは、リクエストごとにEnv
というmap[string]interface{}
が付いていた。 これを使えば、どんなパッケジーにHTTPハンドラーやミドルウェアを入れてもEnv
にあるデータの共有ができた。 しかし文字列のキーは被りやすいし、各ハンドラーでinterface{}
から無理やり変換するのも気持ち悪いと思った。
Go 1.5が出た頃にx/net/context
というパッケージが登場した。今は標準化されてcontext
になっている。context
はgoroutineのキャンセル処理を管理する機能と、”リクエストスコープ”のデータをmapのように扱う機能が付いている。context
のデータ共有機能を使ったら、GojiのEnv
の問題を解決できる。
このcontextパターンでinterface{}
の気持ち悪さから逃れる。
package user
import "context"
// 共有したいデータ。これをcontextに入れる。
type User struct {
ID int
// ...
}
// userKeyは小文字なので他のパッケージはuserKeyを使えない。
// つまり、このパッケージだけはcontextの中のUserがいじられる。
// 他のパッケージと被ることもない。
type userKey struct{}
// contextに入れる
func NewContext(ctx context.Context, u User) context.Context {
return context.WithValue(ctx, userKey{}, u)
}
// contextから出す
func FromContext(ctx context.Context) (User, bool) {
u, ok := ctx.Value(userKey{}).(User)
return u, ok
}
Gojiの一部にcontext
や違うルーターやURLベースのミドルウェアの仕組みを適当に足して、kamiが生まれた。
後悔その1・標準ライブラリとの互換性を破った
早速本題に入ろう。標準ライブラリのHTTPハンドラーはこうなっている。
type HandlerFunc func(ResponseWriter, *Request)
しかしGo 1.7までは*http.Request
にcontext.Context
が入っていなかった。
kamiのハンドラーはこうなっている。
type HandlerFunc func(context.Context, http.ResponseWriter, *http.Request)
これでcontextは使いやすくなるが、kamiのために書いたハンドラーとミドルウェアは他のWAFで使えなくなってしまう。
当時は破るしかなかったと思うが、Go 1.7が出た瞬間にkamiがレガシーソフトになってしまった。Go 1.7が出るまでにkami 2.0を作って標準に従えばよかった。後悔している。
後悔その2・contextの許せないアンチパターンを提供した
kami.Context
か*kami.Mux.Context
を変えることによって、全てのリクエストのベースコンテキストを設定できる。つまり、リクエストスコープじゃないデータを簡単にcontextに入れられる。*sql.DB
とかモデルのレポジトリを入れるために用意してしまった。今やこのcontextの使い方はアンチパターンとして認識されている。やっぱりリクエストの事前にcontextをいじるのはアウトだ。後悔している。
後悔その3・contextを自動的にcancelしなかった
Go 1.7以来、*http.Request
にあるcontext.Context
は自動的にcancelされる。しかしkamiのデフォルトではcancelされない。リクエストが終わったらcancelするという仕組みはとても便利。たとえば途中で切れたリクエストで無駄なDBクエリーをしないためなどに使える。フラグにしなきゃよかった。後悔している。
後悔その4・APIにinterface{}
の乱用
これはGojiを真似てやったが、kamiのHandle系API(kami.Get
, kami.Post
など)はkami.HandlerType
の引数を受け取る。kami.HandlerType
は実はinterface{}
で、実行時に無理やりkami.HandlerFunc
に変換される。http.HandlerFunc
やhttp.Handler
とか色々な型を渡せる。変換できない場合は起動時にパニック。
Goはせっかく型があるので正しく使おう。net/http.Handler
のように、全てを一つのインターフェスにまとめればよかった。後悔している。
後悔その5・Gojiに依存しすぎた
kamiのLogHandlerとAfterwareはそのままgithub.com/zenazn/goji/web/mutil.WriterProxy
を使っている。その長いパスをわざわざインポートしないといけないユーザーは可哀想に思うようになった。kami.WriterProxy
にすればよかった。申し訳ない。後悔している。
後悔その6・新しいバージョンの提供が難しかった
kamiを作った当時はバージョン管理が大変だった。ビルドするたびにGitHubからHEADをダウンロードするユーザーが多かった。互換性のないv2をプッシュしたら、たくさんの人に迷惑かけてしまうことになると思った。ビルドを壊せないために、イマイチなAPIを残すしかなかった。結局は進化出来ないライブラリになった。後悔している。
最後に
かなり暗い記事になったが、私は後悔=学習体験だと思っている。今でもkamiを使っているし、作ってよかったと思う。ただしアンチパターンを使わないように気をつけてほしい。
kamiで失敗したことを全て熟考して、近いうちに新しいkamiを出したい。
皆さんの意見を待っています!