クリスマス期間になると喜ぶ人もいれば悲しむ人もいる。そんな極端な季節の中で、自分が作った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.Requestcontext.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.HandlerFunchttp.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を出したい。
皆さんの意見を待っています!