Help us understand the problem. What is going on with this article?

Revelからechoへの移行メモ

More than 1 year has passed since last update.

モチベーション

  • ここ半年以上Revelが死んでいるので折を見て移行したかった
  • 未熟なGopherとしてRevelの充実した機能に乗っかり続けることへの危機感があった

移行するアプリ

  • 個人で作っているplayground的プロジェクト
  • コードベースはそれほど大きくない
    • 簡単なcontrollerが5つくらい
  • クーロンjobとlogger以外はRevel標準の仕組みを使用
  • かといってRevelにズブズブというわけでもない
    • 小さいアプリなのでRevelの基本的な機能しか使っていなかった

echoを触り始めて数日で書いているので勘違い(echoにこういう機能はない、とか)あるかもしれない。

minimalist Go web frameworkを標榜するechoでは自分で記述を追加する部分が多かった印象(良く言えばカスタマイズ性が高い)。

ホットリロード

:ghost: BEFORE

revelがデフォルトでやってくれていた。

:angel_tone2: AFTER

echoにはない。
いちいちビルドするのもさすがに面倒なのでgodoというGo製タスクランナーを使う。JavascriptでいうところのGulpという認識。
https://github.com/go-godo/godo

アプリは以下のようなディレクトリ(appディレクトリ)で開発している。appディレクトリ内のgo, gohtmlファイルの変更をwatchさせる。

  • :file_folder: app
    • :file_folder: modelsとかhandlersとか
    • :file_folder: views: 中に.gohtmlファイル
    • :page_facing_up: main.go

appディレクトリ直下にGododirというディレクトリを作成し、その中にmain.goファイルを作成する。とりあえずホットリロードができればいいのでシンプル。

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アプリをソース変更後リロードするだけで確認できるようにする

コンフィグ

:ghost: BEFORE

Revelのapp.confファイルに記述。
https://revel.github.io/manual/appconf.html

:angel_tone2: AFTER

echoにはない。
前に見つけて良さそうと思っていたviperを使用することにした。

https://github.com/spf13/viper

Revelのコンフィグは一つのファイルにdevelop, productionごとの設定値、環境変数まで一緒くたに書ける便利さがあった反面、ただ順番に値を書いていくだけ、階層構造も(たぶん)作れなかったので気持ち悪さのようなものがあった。あとRevel側で使用する設定もいろいろあってごちゃごちゃしていた。

viperはymlなりjsonなり好きな記法で階層構造を作って設定を記述できるのが良く、値を取り出す処理もシンプルでいい。

参考
[翻訳+α] Go言語の設定ファイルライブラリ Viper

静的ファイル

:ghost: BEFORE

Revelのroutesファイルに記述
GET /public/*filepath Static.Serve("public")

:angel_tone2: AFTER

echoでは

e := echo.New()
e.Static("/public", "public")

ルーティング

:ghost: BEFORE

routesファイルにRevelの独自記法

routes
GET   /          Home.Index

:angel_tone2: AFTER

main.go
e.GET("/", handlers.HomeIndex)

コントローラ

:ghost: BEFORE

ORMにGormを使っているのでこういうController structを作っていた。

type GormController struct {
    *revel.Controller
    txn *gorm.DB
}

コントローラファイル

homeController.go
type Home struct {
    GormController
}

func (c Home) Index(id int) revel.Result {
    c.RenderArgs = "テンプレートで使うデータ"
    return c.RenderTemplate("home/index.gohtml")
}

:angel_tone2: 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となってもそのエラー内容はどこも表示してくれない。(:pencil2: 次項に追記あり )
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
}

:pencil2: echoのエラーハンドリングについて

エラーをどこも表示してくれないのでラッパーを作った、という点に関してコメントで指摘をいただいたので追記。

echoではhandlerが返すエラーがnilでなかったときecho.HTTPErrorHandlerを呼んでエラーハンドリングしている。

https://github.com/labstack/echo/blob/master/echo.go#L531

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のガイド

Echo advocates centralized HTTP error handling by returning error from middleware and handlers

とある通り、ラッパーを作るのはエラーハンドリングを一箇所に集約しているechoの構成に反している。そもそもラッパーを作っても裏ではHTTPErrorHandlerが動いているので処理の重複になってしまっている。

テンプレート

一番移行に苦労した点。
テンプレートファイルを入れているviewsフォルダは以下のような構成(各ハンドラに対応したディレクトリに分ける)をしている。

  • :file_folder: views
    • :file_folder: home
      • :page_facing_up: index.gohtml
    • :file_folder: work
      • :page_facing_up: index.gohtml

:ghost: BEFORE

controllerで階層を含めてファイル指定。これだけ。
return c.RenderTemplate("home/index.gohtml")

:angel_tone2: 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 で定義した文字列と対応する。

home/index.gohtml
{{ define "home" }}
...
{{ end }}

こういう名前をつける以外にも直接ファイル名で指定することもできる。この場合ディレクトリ階層を指定することはできないのでファイル名はviewsディレクトリ全体で一意にする必要がある(と思う)。

return c.Render(http.StatusOK, "home.gohtml", "")

テンプレートメソッド

:ghost: BEFORE

RevelのCustom Functionを使用
https://revel.github.io/manual/templates.html#CustomFunctions

:angel_tone2: 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

セッション

:ghost: BEFORE

Revelにデフォルトで機能がある
https://revel.github.io/manual/sessionflash.html

:angel_tone2: AFTER

echoにはデフォルトでない。のでミドルウェアを使用
https://github.com/ipfans/echo-session

インターセプター

Revelにあるコントローラの実行前後にメソッド実行を挿入できるやつ

:ghost: BEFORE

https://revel.github.io/manual/interceptors.html

:angel_tone2: AFTER

echoにはもちろんないがcontextのカスタマイズで対応できる(と思う)。

https://echo.labstack.com/guide/context

e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        // handler実行前の何かしらの処理
        defer after()
        return h(cc)
    }
})

func after() {
    // handler実行後の何かしらの処理
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away