Martiniを使うべきではない3つの理由
2014年 7月22日 Golang
Three reasons you should not use Martiniの記事が興味深かったので翻訳してみました。
結構意訳してしまってます。
おかしなとこがあったらごめんなさい。
TL;DR:
MartiniはGoに黒魔術をもたらします。しかしGoのポイントは黒魔術ではなくシンプルであることです。
MartiniのやりかたはGoのエコシステムに沿いません。そのため、muxchainのようなライブラリを試してみましょう。
codegangstaごめんなさい。
記事を書く際にMartiniを名指ししないようにしようとしたのですが、他の方法が思い浮かびませんでした。
Martiniは短期間で大きな注目を集めました。そして、それは何人かの人にGoを試す機会をもたらしたかもしれません。しかし、私はMartiniをオススメしません。いくつかの理由があります。
1. MartiniはGoの型システムに沿わない
Martiniはリクエスト、レスポンスを処理する際、定義したハンドラーをDIします。
このコードを見てください。
func main() {
m := martini.Classic()
m.Get("/bad", func() {
fmt.Println("Did anything happen?")
})
m.Run()
}
後でもう一回見てみますが、これをコンパイルして実行すると、たぶんあなたが思っているような動作をするでしょう。しかし、我々はこんなかんじでリクエストを処理したくないし、net/httpパッケージはこれを許可しません。
この誤りはレスポンスを書く方法がないハンドラーを提供している点にあります。
もうひとつの例です。
func main() {
m := martini.Classic()
m.Get("/myHandler", myHandler)
m.Run()
}
var myHandler = "print this"
これは動きますか?
多少は‥ ちゃんとコンパイルできて、動くように見えます。しかし、実際動かしてみるとpanicになります。初期化の際にハンドラーを設定するのが典型的な使い方なので、これは実際のハンドラーにおける、致命的なpanicが発生するポイントにはならないでしょう。
しかし、これはMartiniにおけるDIパッケージの致命的な問題です。
これはリフレクションの乱用で、型システムにおけるコンパイル時間を増大させます。
これは単にコンパイル時間だけの問題ですが(大惨事になる前の‥)
型システムはGoを選ぶ大きな理由なので、それを受け入れないのであれば、PythonとTwistedを試してみましょう。
2. MartiniはGoのベストパターンの一つを無効にする
ストリーミングデータ処理における統一性とあたりのつけやすさ
あなたがGoを始めたばかりなら、GoでどうやってWebアプリを作りたいかのイメージがあると思います。
これはネイティヴアプローチとして完全に正しいコードのように見えます。
func main() {
m := martini.Classic()
m.Get("/kitten", func() interface{} {
f, _ := os.Open("kitten.jpg")
return f
})
m.Run()
}
ブラウザーで何が見えましたか?
<*os.File Value> 。。
やり直してみましょう。
func main() {
m := martini.Classic()
m.Get("/kitten", func() interface{} {
f, _ := os.Open("kitten.jpg")
defer f.Close()
i, _, _ := image.Decode(f)
return i
})
m.Run()
}
。。何で?
何でこんなことが起こるのかMartiniのリフレクション実装を追うこともできますが、ハンドラーからinterfaceを返したことによって発生したことは明らかです。私が今やったように誰かが空のinterfaceを返すことはありえますね。大抵は正しい型のものを返すでしょうが、間違った型のものを返すこともあると思います。
これは潜在的なリスクに思えます。
もう一度やり直してみましょう。
func main() {
m := martini.Classic()
m.Get("/kitten", func() interface{} {
f, _ := os.Open("kitten.jpg")
defer f.Close()
b, _ := ioutil.ReadAll(f)
return b
})
m.Run()
}
これは動きます。やっと動かすことができました。
でも、注意深い人はこのコードのどこがダメなのか分かると思います。ハンドラーから返す前にすべての画像がメモリ上に展開されます。もしこれが大きな動画ファイルだったら、多分最初のリクエストでメモリを使い切ってしまうでしょう。
他の人がUNIXの哲学に沿ったGoの継承を書いていますが、Martiniは無用なもののためにそれを捨ててしまっています。そう!http.HandlerをMartiniに渡せばいいんです!
しかし、いつもそうしないと、我々のハンドラーの統一性とio.Reader、io.Writerの美しさを失ってしまいます。
私が何を言いたいかというと、このハンドラーの書き方です。
func main() {
m := muxchainutil.NewMethodMux()
m.("GET /kitten", func(w http.ResponseWriter, req *http.Request) {
f, _ := os.Open("kitten.jpg")
defer f.Close()
io.Copy(w, f)
})
http.ListenAndServe(":3000", m)
}
この方法だとioがちゃんと機能して、画像とネットワークに少しのメモリしか使いません。
なぜならio.Readerとio.Writerは始めから終わりまで標準ライブラリを通して使用されるからです。
結局、画像や暗号化、HTMLテンプレートやHTTPのライブラリとシームレスに統合できます。
これはすごい方法です。Goのライブラリを隠して、エコシステムを壊さない方が良いです。
3. Martiniは壊れている
package main
import (
"fmt"
"github.com/go-martini/martini"
)
func main() {
m := martini.Classic()
m.Get("/", func() string {
return "hello world"
})
m.Get("/bad", func() {
fmt.Println("Did anything happen?")
})
m.Run()
}
/badはstdoutに出力するようにルーティングされています。しかし、ステータスコード200を返しているのにもかかわらず、ステータスコード0をロギングしています。どのステータスコードを出力するのが正しいかはわかりませんが、200はnet/httpのデフォルトのレスポンス動作で、0は矛盾しています。
次のコードをみてください。
import (
"io"
"net/http"
"github.com/go-martini/martini"
)
func main() {
m := martini.Classic()
m.Get("/+/hello.world", func(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, req.URL.Path)
})
m.Run()
}
/+/hello.worldにアクセスした場合どうなると思いますか?
そう404が返ります。//hello.worldではどうでしょう?
これは動作します。わかりましたか?Mertiniはこれを正規表現と解釈します。これはGoパッケージとしてのMartiniに存在する欠陥です。なぜなら正規表現として動作させたいのに、そうなりません。しかし、いくつかの正規表現は正しいURLパスになります。
混乱の元ですね。
結論
この記事はMartiniに限ったものではありません。例のために、ひとつのパッケージを選んだにすぎません。
なぜならこれらは至る所で見られ、なぜかを考えるためです。これらが大きな利便性をもたらすのは理解できます。
しかしそれらは最終的に大きなコストを負担しなければならない魔法を用いています。今後たくさんのパッケージがこれらの失敗、エコシステムの汚染を繰り返すでしょう。そして、私が指摘したような見つけにくいバグを含んでいくでしょう。これは我々がGoと進んで行く正しい道ではありません。
私は自分自身でこれを証明したかったため、muxchainというパッケージを作りました。
muxchainとmuxchainutilでMartiniが提供する機能のほとんどを黒魔術なしで実現しました。
すべてはnet/httpの通りです。リフレクションは使ってません。
試してみてどんなかんじか教えてください。まだ新しくていろいろやらなければいけないと思います。
とりわけネーミングについては改善が必要だと思ってます。