isucon3スタッフとして本選運営のお手伝いをしてきましたが、参加者ではないのでisuconについて特に書くことが見つからないmix3です。一つ言えるのはみんな凄すぎでそれに比べて自分は本当になにも出来ない無能なんだなぁと再認識させられました。死にたい。
移植を引き受けた話
isuconでは最初に動く状態のアプリが用意されるのですが、そのアプリは問題が確定した後にまずリファレンス実装(今回はPerl)がされ、その後予選を抜けた人たちが使用していた言語(Perl含むRuby,Python,PHP,NodeJS,Go)に移植する、という流れで作られました。自分はその中でGo言語移植を担当しました。
ちなみにPerl以外の移植言語の中ではPHPぐらいしか触ったことがなく、PHPもかれこれ2,3年は触っておらずすっかり忘れてしまってるので、何の言語を担当しようと何も分からない状態から始まるという「なんで移植担当引き受けちゃったの?」と言われても仕方ないような状態でした。
なんでだろう?きっと暇だった仕事に余裕があったんだね
移植コトハジメ
そんな状態だったので、移植する前に「Goの勉強しないとなぁ」となり、以下のサイトを参考にGoの書き方、Goでwebappを作る場合にどうするのか調べたりしてました。なお、予選コードがあったおかげでベタ移植には困ることが無かったのでとてもとても助かりました。動くコードがあるというのは大事です。
- Go言語の初心者が見ると幸せになれる場所
- Go言語でWebAppの開発に必要なN個のこと
- 予選のコード
- jsonのconfigファイルのロードや、gorilla/muxなどは予選コードを
コピペ参考にしました
- jsonのconfigファイルのロードや、gorilla/muxなどは予選コードを
移植戦略
今回の移植で予選コードに追加して必要だったのは以下3点
- 外部コマンド
- 画像リサイズにリファレンス実装ではImageMagickのconvertコマンドを使用するため
- JSONのレスポンス生成
- 予選とは違いAPIベースだったのでJSONを返す必要があった
- ロングポーリングの実現
これらが出来れば後は予選コードをコピペ参考にすれば移植は出来そうでした
外部コマンド, JSONレスポンス
外部コマンドについてはパッケージドキュメント os/exec 見たまんまです。捻りようもありませんね。
JSONレスポンスはSuper-easy JSON HTTP responses, in Goをパクりました参考にしました。
へーと思ったのがtype Response map[string]interface{} の書き方。mapの値の型をinterface{}にするとinterface{}は何でも受け取れるので自身(この場合Response型)を入れ子にすることが出来るので、いわゆる連想配列やらMapやらと同じ感じで扱える型が出来るわけですね。まあJavaでMap<String, Object>とかする感じでしょうか。
goroutin サイコー
ロングポーリングはGo Language Patterns web/Long-Poll Serverにてgoroutinを使った例があり、これをパクりました参考にしました。
ここで始めてgoroutinを知りました。Goの話を聞くときは大抵並列の話が出るな、と思っていたのですが「なるほど」となりました。たしかにgoroutinすごいです。
- message/channel で排他制御とか考えずにデータをやり取り出来る
- selectでmessage/channelを通してイベントドリブン制御
- go func () { ... } () でさくっと並列処理を走らせる
確かにこれはそういった処理実装するの「簡単だわー」ってなります。
ただまあロングポーリング程度の実装で何が分かる?という向きもあると思うし、これでヒャッホーってなってプロダクション環境で使ったりした日には何か罠踏むんだろうな、とは思いますが…。
Kossyのfilterと例外処理
ここまでで移植は可能だったので割合サクっと移植してしまい本選でもそれをgoの初期アプリとして出しました。が、正直なところ本戦に出した移植アプリは移植としては不完全だったかなぁという思いがありました。
なぜならPerlのリファレンス実装で使われているKossyというフレームワークにはfilterという機能があり、Sinatraルーティングに追加してコントローラ処理の前後に処理を挟めるようになっており、リファレンス実装ではそれを使って、ユーザ情報の取得、ユーザ情報取得失敗時の400エラーを実装していました。抜粋すると以下のような感じ。
filter get_user => sub {
# userの取得してstashに保存 別にundefでも構わない
};
filter require_user => sub {
# stashにuserが無かったら400エラー
};
post '/signup' => sub {
};
get '/image/:image' => [qw/ get_user /] => sub {
# 先に get_user が実行される
};
get '/me' => [qw/ get_user require_user /] => sub {
# 先に get_user, require_user が並びの順で実行される
};
これに対してGoのベタ移植ではgetUser関数を呼んで nilチェックするコードを各コントローラにコピペしていました。「こんなの業務で書いたら殺されるかもしれない」という思いが渦巻くぐらいには良くない書き方だと思います。
それともう一つよく無かったなと思ったのがエラー時のレスポンス処理。Kossyでは$c->halt(500)などとしてExceptionを投げれば後はKossyがそれを捕まえてエラーレスポンスを返してくれます。これがGoのベタ移植では必要なときに毎度レスポンスを設定してreturnして関数を抜けるという素朴なことをしていました。
この「Kossyのfilter」「エラー時のレスポンス処理」2点が移植していてすごくもやもやしていたので、本選のアプリとしては使わないかもしれないけど「こうしたほうが良いんじゃないか?」というのを盛りこんだ実装も別で書いたりしていました
Goにおける例外処理
Goにはtry-catch-finallyがない代わりにpanic/recoverというものがあり、deferと組み合わせて例外処理っぽいことが出来ます Defer, Panic, and Recover
- defer
- 関数を抜けた後に実行する遅延処理を定義出来る
- panic
- 関数を即抜ける perlで言うとdieみたいな感じ またそのときに遅延処理を実行する
- recover
- panicから復帰する perlで言うとevalみたいな感じ deferの中で使うのが基本?
組み合わせて書くと例えば以下のような感じになります
func main() {
// (1) mainを抜けると実行される処理の定義
defer func() {
// (3) panicから復帰する
if err := recover(); err != nil {
fmt.Printf("recover: %v\n", err)
}
}()
// (2) mainから即抜ける
panic("panic")
}
// (4) output
// recover: panic
defer panic/recoverを使ったエラーレスポンス処理
これを使ってwebappのエラー処理をするならこうなるかな?というのを書いてみたのが以下になります(ちなみに以下のサンプルを書いていてswitch文にbreak使わなくてもよいことを初めて知りました)
package main
import (
"net/http"
"github.com/gorilla/mux"
"fmt"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/", handler).Methods("GET")
r.HandleFunc("/{code}", handler).Methods("GET")
http.Handle("/", r)
http.ListenAndServe(":5000", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
defer func () {
if err := recover(); err != nil {
var code int
switch err {
case "400": code = http.StatusBadRequest
case "404": code = http.StatusNotFound
case "500": fallthrough
default : code = http.StatusInternalServerError
}
http.Error(w, http.StatusText(code), code)
}
} ()
vars := mux.Vars(r)
if code := vars["code"]; code != "" {
panic(code)
}
fmt.Fprint(w, "OK")
}
// [output]
// http://loclahost:5000/ => OK
// http://loclahost:5000/400 => Bad Request
// http://loclahost:5000/404 => Not Found
// http://loclahost:5000/500 => Internal Server Error
// http://loclahost:5000/999 => Internal Server Error
panicの引数はinterface{}型なのでつまり何でも渡せるので、自前の型を定義して便利機能を付けておくとrecover()で受け取った後が捗りそうですね。
例外処理自体はこれで良いのですが、ただこのままだとdeferをコントローラ毎に書いて行かないと行けなくなるので、当然それは嬉しくありません。
func main() {
r := mux.NewRouter()
r.HandleFunc("/hoge", hoge).Methods("GET")
r.HandleFunc("/fuga", fuga).Methods("GET")
r.HandleFunc("/bar", bar ).Methods("GET")
http.Handle("/", r)
http.ListenAndServe(":5000", nil)
}
func hoge(w http.ResponseWriter, r *http.Request) {
defer func () {
if err := recover(); err != nil {
// ...
}
} ()
fmt.Fprint(w, "hoge")
}
func fuga(w http.ResponseWriter, r *http.Request) {
defer func () {
if err := recover(); err != nil {
// ...
}
} ()
fmt.Fprint(w, "hoge")
}
func bar(w http.ResponseWriter, r *http.Request) {
.
.
.
// deferコピペしないといけないのでは嬉しく無い
で、どうしようか考えた結果、defer,recoverするラッパー関数にコントローラを渡してあげれば良いんじゃないかと考えてみました、具体的には以下のような感じ。
func main() {
r := mux.NewRouter()
r.HandleFunc("/hoge", wrapper(hoge).Methods("GET")
r.HandleFunc("/fuga", wrapper(fuga).Methods("GET")
r.HandleFunc("/bar", wrapper(bar ).Methods("GET")
http.Handle("/", r)
http.ListenAndServe(":5000", nil)
}
func wrapper(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func (w http.ResponseWriter, r *http.Request) {
defer func () {
if err := recover(); err != nil {
// ....
}
} ()
handler(w, r)
}
}
func hoge(w http.ResponseWriter, r *http.Request) {
panic("hoge")
}
func fuga(w http.ResponseWriter, r *http.Request) {
panic("fuga")
}
func bar(w http.ResponseWriter, r *http.Request) {
.
.
.
こうすることで例外処理の復帰部分をラッパー関数に押し込むことができました。例外処理のためにHandleFuncに渡すコントローラはラッパー関数を必ず噛ませないといけないルールが出来てしまうので、そこはちょっとアレな感じですが。
そしてさらにこのラッパー関数を多段にするとKossyのfilterっぽいことが出来そうです。(Kossyでは配列で順序を自由に設定出来るので、実際にはそれっぽく出来るってだけですけども)
package main
import (
"net/http"
"github.com/gorilla/mux"
"fmt"
)
type Stash map[string]interface{}
func main() {
r := mux.NewRouter()
r.HandleFunc("/pass", base(pass(handler))).Methods("GET")
r.HandleFunc("/{code}", base( handler )).Methods("GET")
r.HandleFunc("/", base( handler )).Methods("GET")
http.Handle("/", r)
http.ListenAndServe(":5000", nil)
}
func base(handler func(w http.ResponseWriter, r *http.Request, s Stash)) func(w http.ResponseWriter, r *http.Request) {
return func (w http.ResponseWriter, r *http.Request) {
defer func () {
if err := recover(); err != nil {
var code int
switch err {
case "400": code = http.StatusBadRequest
case "404": code = http.StatusNotFound
case "500": fallthrough
default : code = http.StatusInternalServerError
}
http.Error(w, http.StatusText(code), code)
}
} ()
handler(w, r, Stash{})
}
}
func pass(handler func(w http.ResponseWriter, r *http.Request, s Stash)) func(w http.ResponseWriter, r *http.Request, s Stash) {
return func (w http.ResponseWriter, r *http.Request, s Stash) {
s["msg"] = "PASS"
handler(w, r, s)
}
}
func handler(w http.ResponseWriter, r *http.Request, s Stash) {
vars := mux.Vars(r)
if code := vars["code"]; code != "" {
panic(code)
}
if msg, ok := s["msg"]; !ok || msg == "" {
fmt.Fprint(w, "OK")
} else {
fmt.Fprint(w, msg)
}
}
// [output]
// http://loclahost:5000/ => OK
// http://loclahost:5000/pass => PASS
// http://loclahost:5000/400 => Bad Request
// http://loclahost:5000/404 => Not Found
// http://loclahost:5000/500 => Internal Server Error
// http://loclahost:5000/999 => Internal Server Error
どうでしょうか。ラッパー関数を多段にすることでfilterっぽい感じが表現出来たり、panic/recoverでエラーレスポンスをサクッと作れたり、なんとなくフレームワーク支援を受けて書いてるような気分に浸れそうな感じがしてきませんかね。
予選コードから考えるとちょっと手を入れ過ぎ感が否めなかったり、移植と言っても機能を満たせば良いわけでフレームワークがやってる処理までそれっぽく移植する必要はないので、結局は本戦の移植アプリにはしませんでしたが、実際にGoでこういうの書く場合はこれぐらいの工夫はしても良いんじゃないかなと思ったりします。ただでさえGoだとまじめにエラーハンドリングする必要があって縦にコードが伸び易いので、少しでもコードを減らす工夫はしたほうが良いように思います。
なお、これを適用することによって900行あった本戦の移植アプリが800行になりました。やったねたえちゃん!100行も減ったよ!でもperlはそもそも400行ありませんでしたね。切ない。
ということで
自己満足も満たしつつ移植作業が出来たので楽しかったです。なによりgoroutinを知れたのが良かったです。「Goが良いというのはこういうことか」みたいなのが少しだけ実感を持って感じられました。
おわりに
今回はisuconにスタッフとして関わったけれどもisucon自体には一度も参加してないので、来年isuconあったら予選ぐらいは参加してボコボコにされてこようかな、とかそんなこと考えるくらいにはisucon3の盛り上がりが凄かったです。
isucon関係者、参加者のみなさま、本当にお疲れさまでした。