モチベーション
- ここ半年以上Revelが死んでいるので折を見て移行したかった
- 未熟なGopherとしてRevelの充実した機能に乗っかり続けることへの危機感があった
移行するアプリ
- 個人で作っているplayground的プロジェクト
- コードベースはそれほど大きくない
- 簡単なcontrollerが5つくらい
- クーロンjobとlogger以外はRevel標準の仕組みを使用
- かといってRevelにズブズブというわけでもない
- 小さいアプリなのでRevelの基本的な機能しか使っていなかった
echoを触り始めて数日で書いているので勘違い(echoにこういう機能はない、とか)あるかもしれない。
minimalist Go web framework
を標榜するechoでは自分で記述を追加する部分が多かった印象(良く言えばカスタマイズ性が高い)。
ホットリロード
BEFORE
revelがデフォルトでやってくれていた。
AFTER
echoにはない。
いちいちビルドするのもさすがに面倒なのでgodoというGo製タスクランナーを使う。JavascriptでいうところのGulpという認識。
https://github.com/go-godo/godo
アプリは以下のようなディレクトリ(appディレクトリ)で開発している。appディレクトリ内のgo, gohtmlファイルの変更をwatchさせる。
-
app
- modelsとかhandlersとか
- views: 中に.gohtmlファイル
- main.go
appディレクトリ直下にGododirというディレクトリを作成し、その中にmain.goファイルを作成する。とりあえずホットリロードができればいいのでシンプル。
package main
import do "gopkg.in/godo.v2"
func tasks(p *do.Project) {
p.Task("server", nil, func(c *do.Context) {
c.Start("main.go", nil)
}).Src("**/*.{go,gohtml}").
Debounce(3000)
}
func main() {
do.Godo(tasks)
}
あとはappディレクトリでgodo server -w
コマンドを実行すればok
注意点としてはファイル名パターンの指定でカンマ,の後にスペースを空けるとgohtmlファイルの変更が認識されなくなる。
o Src("**/*.{go,gohtml}")
x Src("**/*.{go, gohtml}")
参考
Docker内のGo製Webアプリをソース変更後リロードするだけで確認できるようにする
コンフィグ
BEFORE
Revelのapp.confファイルに記述。
https://revel.github.io/manual/appconf.html
AFTER
echoにはない。
前に見つけて良さそうと思っていたviperを使用することにした。
Revelのコンフィグは一つのファイルにdevelop, productionごとの設定値、環境変数まで一緒くたに書ける便利さがあった反面、ただ順番に値を書いていくだけ、階層構造も(たぶん)作れなかったので気持ち悪さのようなものがあった。あとRevel側で使用する設定もいろいろあってごちゃごちゃしていた。
viperはymlなりjsonなり好きな記法で階層構造を作って設定を記述できるのが良く、値を取り出す処理もシンプルでいい。
参考
[翻訳+α] Go言語の設定ファイルライブラリ Viper
静的ファイル
BEFORE
Revelのroutesファイルに記述
GET /public/*filepath Static.Serve("public")
AFTER
echoでは
e := echo.New()
e.Static("/public", "public")
ルーティング
BEFORE
routesファイルにRevelの独自記法
GET / Home.Index
AFTER
e.GET("/", handlers.HomeIndex)
コントローラ
BEFORE
ORMにGormを使っているのでこういうController structを作っていた。
type GormController struct {
*revel.Controller
txn *gorm.DB
}
コントローラファイル
type Home struct {
GormController
}
func (c Home) Index(id int) revel.Result {
c.RenderArgs = "テンプレートで使うデータ"
return c.RenderTemplate("home/index.gohtml")
}
AFTER
Revelではcontrollerと呼んでいたがGoっぽくhandlerと呼ぶことに。
func HomeIndex(c echo.Context) error {
id := c.Param("id")
return c.Render(http.StatusOK, "home", "テンプレートで使うデータ")
}
上記のreturn c.Render(...
はechoのドキュメント通りの記述だがここでerr != nil
となってもそのエラー内容はどこも表示してくれない。( 次項に追記あり )
Revelでは丁寧にエラーを表示してくれていたがechoは以下のメッセージをブラウザに表示するだけだった。
{"message": "Internal Server Error"}
なのでこのようなラッパーを作って各handlerから使うようにした。 →あまりよくなかった
func render(c echo.Context, template string, vv viewvars) error {
err := c.Render(http.StatusOK, template, vv)
if err != nil {
...
}
return err
}
echoのエラーハンドリングについて
エラーをどこも表示してくれないのでラッパーを作った、という点に関してコメントで指摘をいただいたので追記。
echoではhandlerが返すエラーがnilでなかったときecho.HTTPErrorHandlerを呼んでエラーハンドリングしている。
HTTPErrorHandlerにはデフォルトでDefaultHTTPErrorHandlerがセットされる。
上記でエラー発生時に画面に{"message": "Internal Server Error"}
しか表示されないと書いたがそれはDefaultHTTPErrorHandlerが返していたものだった。
DefaultHTTPErrorHandlerは詳細なエラー内容を表示してくれないのでラッパーを作ったがそれよりはHTTPErrorHandlerをカスタマイズしたほうがいいのだろう。
e.HTTPErrorHandler = func(err error, c echo.Context) {
fmt.Println(err) // 標準出力へ
c.JSON(http.StatusInternalServerError, err.Error()) // ブラウザ画面へ
}
Echo advocates centralized HTTP error handling by returning error from middleware and handlers
とある通り、ラッパーを作るのはエラーハンドリングを一箇所に集約しているechoの構成に反している。そもそもラッパーを作っても裏ではHTTPErrorHandlerが動いているので処理の重複になってしまっている。
テンプレート
一番移行に苦労した点。
テンプレートファイルを入れているviewsフォルダは以下のような構成(各ハンドラに対応したディレクトリに分ける)をしている。
-
views
-
home
- index.gohtml
-
work
- index.gohtml
-
home
BEFORE
controllerで階層を含めてファイル指定。これだけ。
return c.RenderTemplate("home/index.gohtml")
AFTER
基本はechoのドキュメントに従う。
テンプレートをパースするところ
t := &Template {
template.Must(template.ParseGlob("views/**/*.gohtml")),
}
e.Renderer = t
handler内でレンダーをするところで渡している home はテンプレートの名前。
func HomeIndex(c echo.Context) error {
return c.Render(http.StatusOK, "home", "テンプレートで使うデータ")
}
この名前はテンプレートの中で define で定義した文字列と対応する。
{{ define "home" }}
...
{{ end }}
こういう名前をつける以外にも直接ファイル名で指定することもできる。この場合ディレクトリ階層を指定することはできないのでファイル名はviewsディレクトリ全体で一意にする必要がある(と思う)。
return c.Render(http.StatusOK, "home.gohtml", "")
テンプレートメソッド
BEFORE
RevelのCustom Functionを使用
https://revel.github.io/manual/templates.html#CustomFunctions
AFTER
テンプレートをパースするところに記述を追加する必要。
f := template.FuncMap{}
t := &Template{
templates: template.Must(template.New("").Funcs(f).ParseGlob("views/**/*.gohtml")),
}
e.Renderer = t
Template and custom function; panic: function not defined
Revelにデフォルトで入っていたfunctionで使いたいものはこのあたりを参考に移植した
https://github.com/revel/revel/blob/master/template.go#L45
セッション
BEFORE
Revelにデフォルトで機能がある
https://revel.github.io/manual/sessionflash.html
AFTER
echoにはデフォルトでない。のでミドルウェアを使用
https://github.com/ipfans/echo-session
インターセプター
Revelにあるコントローラの実行前後にメソッド実行を挿入できるやつ
BEFORE
AFTER
echoにはもちろんないがcontextのカスタマイズで対応できる(と思う)。
e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// handler実行前の何かしらの処理
defer after()
return h(cc)
}
})
func after() {
// handler実行後の何かしらの処理
}