35
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

今となって後悔しているkamiのこと

Last updated at Posted at 2017-12-20

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

35
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?