アルパカ三銃士

〜アルパカに酔いしれる獣たちへ捧げる〜

echo.Context を最大限に活用する

この記事は Go Advent Calendar 2017 の記事です。

僕が Go で Web Application を開発するときに主に利用するのが labstack/echo です。

github.com

その際に副産物として生産された記事が Qiita にあがっているのでそちらも参照してもらえると嬉しいです。

qiita.com

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 をラップするようなガイドが紹介されているので、そちらも併せてご覧ください。

echo.labstack.com

あと、宣伝ですが Go もバリバリ書くようなエンジニアが参加する YAPC が沖縄で開催されるので、もし宜しければ参加しませんか!?

yapcjapan.org