この記事は Go Advent Calendar 2017 の記事です。
僕が Go で Web Application を開発するときに主に利用するのが labstack/echo
です。
その際に副産物として生産された記事が Qiita にあがっているのでそちらも参照してもらえると嬉しいです。
echo.Context を活用する
タイトル通りに最大限に活用されていなければ、本当に申し訳ありません...
さて、 echo で Controller 側のコードを書く際に良く用いられるのが echo.Context
です。例えば良く扱う例として、api を作成する際に、アプリケーション側でバリデーションも同時に行うことを想定するならば、先に go-playground/validator
を利用したバリデーションを行うためにセットアップを行い、コントローラの処理を書くでしょう。
package main import ( "fmt" "net/http" "github.com/labstack/echo" validator "gopkg.in/go-playground/validator.v9" ) type User struct { Name string `json:"name" form:"name" query:"name" validate:"required"` Email string `json:"email" form:"email" query:"email" validate:"required"` } type Validator struct { validator *validator.Validate } func (v *Validator) Validate(i interface{}) error { return v.validator.Struct(i) } func main() { e := echo.New() e.Validator = &Validator{validator: validator.New()} e.POST("/post_profile", func(c echo.Context) error { u := new(User) if err := c.Bind(u); err != nil { return c.String(http.StatusBadRequest, "Request is failed: "+err.Error()) } if err := c.Validate(u); err != nil { return c.String(http.StatusBadRequest, "Validate is failed: "+err.Error()) } fmt.Println(u) return c.String(http.StatusOK, "OK") }) // Start server e.Logger.Fatal(e.Start(":3000")) }
コントローラの処理として次のような部分のことを指しています。
e.GET("/path", func(c echo.Context) error { return nil }) e.POST("/path", func(c echo.Context) error { return nil })
ここで問題なのは、api のパスが増えるたびに同様の処理をコントローラのコードに書かなくてはいけないため、本当に書きたい処理以外のコードが増えていきます。この場合増えていくコードは以下の部分になります。
if err := c.Bind(u); err != nil { return c.String(http.StatusBadRequest, "Request is failed: "+err.Error()) } if err := c.Validate(u); err != nil { return c.String(http.StatusBadRequest, "Validate is failed: "+err.Error()) }
このような問題を解決するために echo.Context
を構造体でラップして活用していきます。
echo.Context をラップする
ラップしていきましょう。この時にリクエスト内容を取得する Bind
とバリデーションを行う Validate
を、ラップした構造体が持つメソッドとして書いてあげればよりシンプルになります。
package main import ( "fmt" "net/http" "github.com/labstack/echo" validator "gopkg.in/go-playground/validator.v9" ) type User struct { Name string `json:"name" form:"name" query:"name" validate:"required"` Email string `json:"email" form:"email" query:"email" validate:"required"` } type Validator struct { validator *validator.Validate } func (v *Validator) Validate(i interface{}) error { return v.validator.Struct(i) } // echo.Context をラップする構造体を定義する type Context struct { echo.Context } // Bind と Validate を合わせたメソッド func (c *Context) BindValidate(i interface{}) error { if err := c.Bind(i); err != nil { return c.String(http.StatusBadRequest, "Request is failed: "+err.Error()) } if err := c.Validate(i); err != nil { return c.String(http.StatusBadRequest, "Validate is failed: "+err.Error()) } return nil } func main() { e := echo.New() e.Validator = &Validator{validator: validator.New()} // echo.Context をラップして扱うために middleware として登録する e.Use(func(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { return h(&Context{c}) } }) e.POST("/post_profile", func(c echo.Context) error { cc := c.(*Context) // キャスト u := new(User) if err := cc.BindValidate(u); err != nil { return err } fmt.Println(u) return cc.String(http.StatusOK, "OK") }) // Start server e.Logger.Fatal(e.Start(":3000")) }
こうすることでコントローラが増えていく度に書くコードの量を抑えることができるので、読みやすいコードになるでしょう。
しかし、毎回 *Context
をキャストするのもなんだか煩わしいです。
+α
これは好みですが僕は以下のようなコードを追加して毎回キャストの処理を書かないようにしています。
type callFunc func(c *Context) error func c(h callFunc) echo.HandlerFunc { return func(c echo.Context) error { return h(c.(*Context)) } }
これらを宣言することで最終的なコードは以下のようになります。
package main import ( "fmt" "net/http" "github.com/labstack/echo" validator "gopkg.in/go-playground/validator.v9" ) type User struct { Name string `json:"name" form:"name" query:"name" validate:"required"` Email string `json:"email" form:"email" query:"email" validate:"required"` } type Validator struct { validator *validator.Validate } func (v *Validator) Validate(i interface{}) error { return v.validator.Struct(i) } // echo.Context をラップする構造体を定義する type Context struct { echo.Context } // Bind と Validate を合わせたメソッド func (c *Context) BindValidate(i interface{}) error { if err := c.Bind(i); err != nil { return c.String(http.StatusBadRequest, "Request is failed: "+err.Error()) } if err := c.Validate(i); err != nil { return c.String(http.StatusBadRequest, "Validate is failed: "+err.Error()) } return nil } type callFunc func(c *Context) error func c(h callFunc) echo.HandlerFunc { return func(c echo.Context) error { return h(c.(*Context)) } } func main() { e := echo.New() e.Validator = &Validator{validator: validator.New()} // echo.Context をラップして扱うために middleware として登録する e.Use(func(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { return h(&Context{c}) } }) e.POST("/post_profile", c(func(c *Context) error { u := new(User) if err := c.BindValidate(u); err != nil { return err } fmt.Println(u) return c.String(http.StatusOK, "OK") })) // Start server e.Logger.Fatal(e.Start(":3000")) }
これで毎度キャストするコードを書かなくて済むので、行数を減らすことが可能です。
最後に
公式に echo.Context
をラップするようなガイドが紹介されているので、そちらも併せてご覧ください。
あと、宣伝ですが Go もバリバリ書くようなエンジニアが参加する YAPC が沖縄で開催されるので、もし宜しければ参加しませんか!?