Go言語は標準の net/http
が結構よくできてるので、WEBフレームワークはなくてもいいだとかそういう話がありますし、 net/http
をラップした俺俺フレームワークが大量に作られています。
というわけで、俺も遅ればせながら俺俺フレームワークを作りました。
読み方は「シードル」。お酒の名前つけるのが慣習ですからね。よくあるこんな感じ。
1package main
2
3import (
4 "github.com/yuin/cidre"
5 "net/http"
6)
7
8func main() {
9 app := cidre.NewApp(cidre.DefaultAppConfig())
10 root := app.MountPoint("/")
11
12 root.Get("show_welcome", "welcome", func(w http.ResponseWriter, r *http.Request) {
13 app.Renderer.Text(w, "Welcome!")
14 })
15
16 app.Run()
17}
特徴
-
よくあるSinatraチックなAPI
-
できるだけ標準インタフェースを使用。いろんな既存ライブラリとの相性が良い。
-
他の薄いフレームワークではオプションな機能も一部内包。
- セッション、フラッシュメッセージ
- レイアウト機能をサポートした
html/template
のラッパー
-
フック機能を提供していて、より柔軟に外部から拡張可能。
開発経緯
そもそもPythonistaの御多聞にもれず、2と3のはざまでもだえる中でGo言語書くことが多くなってたんですね。んでWEBもGo言語でさらっと書きたい、と。
Go言語のWEBフレームワークはいっぱいあって、軽量だとMartiniだとかGinだとかnegroniだとか、重量級だとbeegoだとかrevelだとか。俺の好みとしてやっぱりシンプルなものが好きなので軽量フレームワークを使いたいところだけど、Martiniはtoo magicだし、Ginは40 times fasterってのが詐欺っぽいし、negroniはツールであってフレームワークじゃないと言っているしで、あんまりしっくりくるのがありませんでした。
なので自分が最低限必要と思う機能を組み込んだフレームワークを作ったわけです。ミドルウェアで対応できるけど組み込まれてたほうが楽だし。SPAが流行ってるって言ってもさらっと作るときはフラッシュメッセージが楽だし、設定は外出ししときたいし、とか。
基本的に http.Handler
(もしくは http.HandlerFunc
)で構成されるので他のいわゆる「ミドルウェア」と呼ばれているものもすんなり組み込めます。せっかく組み込みライブラリがよくできてるんだから、なるべくフレームワーク特有のことは覚えたくないのもある。
テンプレートエンジンやSessionストアやロガーはInterfaceなので差し替え可能です。
あと、適当に今風のプロジェクトページ作りました。
開発中に思ったこと
俺俺フレームワークを書きたい方のために、cidreを書いてる時に思ったこと、検討したことを書いておきます。
Contextの持ち方
どのフレームワークも Context
という構造体がだいたいある。これは入れ子になってる http.Handler
間でデータを受け渡すのが主目的だ。
でContextの考え方は3種類ある。
-
http.Handler
インタフェースを使わず独自インタフェースをつくって引数として渡す。
-
- Gorilla context のようなスレッドローカル変数を使う。
-
http.Handler#ServeHTTP
の引数であるhttp.ResponseWriter
かhttp.Request
のどこかに埋め込む。
1はGinやnegroniなど大半のフレームワークが採用している方法。これはこれでシンプルでよい。ただし独自インタフェースになる。
2はスレッドローカルにするためにgoなのにLock, Unlockが走りまくるのが難点だが見た目すごくクリーン。
3は生成時にトリックが必要だけど標準インタフェースを使えて、ロックも発生しない。というわけでcidreは3の方式をとっている。Goで外部からオブジェクトを埋め込むためには
- それがインタフェースで
- Public
じゃないといけない。というわけで http.Request#Body
に埋め込んでいる。
拡張性
Writing HTTP Middleware in Go という記事があるように、 http.Handler
をPythonのWSGIミドルウェアのように扱う、というのは標準的な考え方だろう。
ただ、結局この方式はただのフィルタであって柔軟性がない。HTTPボディを書く前に処理を差し込みたい、とかできない。正確にはできないことないけどめんどくさい(独自ResponseWriterを作って次のミドルウェアに渡すことになる)。Martiniでは独自ResponseWriterにコールバックが設定できるようになっていてHTTPボディ書く前にヘッダ書くというのができる。
でも結局そういうポイントって随所にあって、統一的に扱える仕組みがあったほうがよいと思う。のでHookの仕組みをつくってサーバ起動時、とかいろんなところをフックできるようにしてある。
設定オブジェクト
これは何を今さら、な話で設定を表すオブジェクトをどう扱うか、ということ。例えば以下のようなstructがあるとして
1type Config struct {
2 Host string
3 Port int
4 Timeout time.Duration
5}
これにどうデフォルト値を適応するかっていうこと。
スクリプト言語ならundefinedなりnullなりnilなり未初期化を表す共通の値があるので、よいのだがCやGoではintは初期値0だし、0と設定したのか未設定なのかわからない。
なので以下のようなデフォルト設定を返す関数をつくってそれに設定を追加していく形がよいと思う。
1func DefaultConfig(init ...func(*Config)) *Config {
2 self := &Config {
3 Host: "localhost",
4 Port: "8080",
5 Timeout: 180 * time.Second,
6 }
7 if len(init) > 0 {
8 init[0](self)
9 }
10 return self
11}
12
13config := DefaultConfig()
14config.Timeout = 0
あと利便性のためこういう書き方もできるようにしてある。
1app := NewApp(DefaultConfig(func (config *Config){
2 config.Timeout = 0
3})
といろいろあるけどこんなところで。
今後について
今後も細々メンテしていくつもりですし、ミドルウェアなんかも追加していきたいなあと思っています。やっぱGo言語はさらっと書けるそれなりに速いし、いいっす。