Go
golang
echo

golangのechoでのREST API実装を整理する


はじめに

Webサービス開発 && REST APIの勉強にgolangのechoを使っているのですが、API仕様としてURL+メソッド毎に関数登録出来るので、REST APIとの相性がいいのではないかと思います。


regist_api.go

group := e.Group("/api/")

group.GET(uri, get_handler_fnc)
group.POST(uri, post_handler_fnc)
group.PUT(uri, put_handler_fnc)
group.DELETE(uri, delete_handler_fnc)

ただこういう関数登録系のAPIって、何も考えずに作ると関数の量が凄いことになるので、APIの仕様をチーム間で調整しながら開発してると結構カオスなことになりそう。

なので、ある程度汎用的な形で実装してみました。


設計

基本方針はこんな感じで考えました。


  1. GET/POST/PUT/DELETEはURI毎にHandleXXXで実装する。

  2. HandleXXXの実体はFactory class的なものに生成させ、main処理に依存しないようにする。

  3. 1と2をつなぐためのインターフェースを定義する。

3の定義はechoの処理を活かして必要なものだけ実装・登録する形でもいいし、GET/POST/PUT/DELETEのデフォルト関数を用意して必要に応じてoverrideする形でもいいですね。

クラス図は前者のイメージ

class.png

ファイル構成はこんな感じ。

main.go

controller/
|
+- handle_xxx.go -- REST APIの実処理はこれで統一
+- handle_Logout.go
+- handle_passphrase.go
+- handle_passphraseInfo.go
+- handle_userAccount.go
|
`- msg_handler.go -- interface, factory classの定義


実装


echoの処理を活かした形

goのinterface定義はこんな感じで行います。


controller/msg_handler.go

type Method int

const (
METHOD_GET Method = iota
METHOD_POST
METHOD_PUT
METHOD_DELETE
)

type MsgHandler interface {
GetHandlerFunc(method Method) echo.HandlerFunc
}


これを継承して各Handleクラスを作成します。定義の有無を判別する為に、ちょっと回りくどい実装になってしまった。


controller/handle_passphraseInfo.go

//継承用の構造体定義。メンバーが必要なら追加します。

type HandlePassphraseInfo struct {
}

//頭にthis *構造体を付けてあげると、構造体がthis参照できます。
func (this *HandlePassphraseInfo) GetHandlerFunc(method Method) echo.HandlerFunc{
//実装したメソッドを返してあげる
switch method {
case METHOD_GET: return this.get
case METHOD_PUT: return this.put
case METHOD_DELETE: return this.deleteMethod
default: return nil
}
}

//bodyがJSONフォーマットなら、c.JSON(ステータスコード, res)で応答
func (this *HandlePassphraseInfo) get(c echo.Context) error {
/*実処理*/
return c.JSON(http.StatusOK, res)
}

//他のメソッドも同じような感じで定義


上記のようにガツガツ実装したhandleをfactoryで生成します。


controller/msg_handler.go

func MsgHandlerFactory() map[string]MsgHandler {

var results map[string]MsgHandler = map[string]MsgHandler{"passphrase":&HandlePassphrase{}, "passphraseInfo":&HandlePassphraseInfo{}, "userAccount":&HandleUserAccount{}, "logout":&HandleLogout{}}
return results
}

最後にmainで登録。


main.go

    e = echo.New()

// regist handler
group := e.Group("/api/")
msg_handler := controller.MsgHandlerFactory()
for uri, instance := range msg_handler {
//Set method GET
if handler_fnc := controller.GetEchoHandler(instance, controller.METHOD_GET); handler_fnc != nil {
group.GET(uri, handler_fnc)
}

if handler_fnc := controller.GetEchoHandler(instance, controller.METHOD_POST); handler_fnc != nil {
group.POST(uri, handler_fnc)
}

if handler_fnc := controller.GetEchoHandler(instance, controller.METHOD_PUT); handler_fnc != nil {
group.PUT(uri, handler_fnc)
}

if handler_fnc := controller.GetEchoHandler(instance, controller.METHOD_DELETE); handler_fnc != nil {
group.DELETE(uri, handler_fnc)
}
}



override形式

interface定義は少し増えますが、その分他のコードがすっきりします。

interfaceはこんな感じ。GetHandlerFuncがいらなくなりました。


controller/msg_handler.go

type MsgHandlerIf interface {

Get(c echo.Context) error
Post(c echo.Context) error
Put(c echo.Context) error
Delete(c echo.Context) error
}

type MsgHandler struct {
}
func (this MsgHandler) Get(c echo.Context) error {return c.NoContent(http.StatusMethodNotAllowed)}
func (this MsgHandler) Put(c echo.Context) error {return c.NoContent(http.StatusMethodNotAllowed)}
func (this MsgHandler) Post(c echo.Context) error {return c.NoContent(http.StatusMethodNotAllowed)}
func (this MsgHandler) Delete(c echo.Context) error {return c.NoContent(http.StatusMethodNotAllowed)}

//return: map[uri]MsgHandler
func MsgHandlerFactory() map[string]MsgHandlerIf {
var results map[string]MsgHandlerIf = map[string]MsgHandlerIf{"passphrase":&HandlePassphrase{}, "passphraseInfo":&HandlePassphraseInfo{}, "userAccount":&HandleUserAccount{}, "logout":&HandleLogout{}}
return results
}


継承用のコードは構造体の先頭に親を記載します。後はechoの処理を活かした形で実装したget/post/put/deleteをGet/Post/Put/Deleteに変えればOK。


handle_passphraseInfo.go

type HandlePassphraseInfo struct {

MsgHandler
}

そしてmain。めっちゃすっきりしました。


main.go

    e = echo.New()

// regist handler
group := e.Group("/api/")
msg_handler := controller.MsgHandlerFactory()
for uri, instance := range msg_handler {
group.GET(uri, instance.Get)
group.POST(uri, instance.Post)
group.PUT(uri, instance.Put)
group.DELETE(uri, instance.Delete)
}