まっしろけっけ

めもてきなやーつ

golangのフレームワークrevelを使用して掲示板っぽいものを作ってみる

はじめに

今回は golang の revel framework を使用して掲示板っぽいやつを作ってみる。
掲示板っぽいと言っても基本的には APIJson を返すことにする。
しかし html を返す場合もやることはほぼ変わらない。

今回作成する API は一般的な掲示板でいうスレッドは存在せず
レスのみを扱う。(今後スレッド対応やログイン対応等を行っていくかもしれない)
endpoint は下記のようにする

http method path 説明
GET /comments 一覧
GET /comments/:id 詳細
POST /comments 登録
DELETE /comments/:id 削除

revel is 何 ?

revel は高機能な重量級の framework で Ruby で言うと sinatra というより
Rails に近い framework になります。

環境

$ go version
go version go1.4.2
$ mysql --version
mysql  Ver 14.14 Distrib 5.6.13

revel を install する

下記で install できる

$ go get github.com/revel/revel # revel framework を取得
$ go get github.com/revel/cmd/revel # revel command を取得

プロジェクトを作成してみる

とりあえずハロワが表示されるとこまで進める。

$ revel new github.com/shiro16/golang-bbs # rails new みたいなやつ
~
~ revel! http://revel.github.io
~
Your application is ready:
   ${GOPATH}/src/github.com/shiro16/golang-bbs

You can run it with:
   revel run github.com/shiro16/golang-bbs
$ cd ${GOPATH}/src/github.com/shiro16/golang-bbs
$ revel run # applicaton の起動

これで「http://localhost:9000/」にアクセスすると
「It works!」が表示される。

API の endpoint を定義する

routing を追加する

routing は「config/routes」で管理されている
下記のように編集する。
今回の Api の path は「/api/v1」をベースにする

GET     /                                       App.Index
+ GET     /api/v1/comments                        ApiV1Comments.Index
+ GET     /api/v1/comments/:id                    ApiV1Comments.Show
+ POST    /api/v1/comments                        ApiV1Comments.Create
+ DELETE  /api/v1/comments/:id                    ApiV1Comments.Delete
controller を作成する

今回は api で共通して使うであろう Error 処理等をまとめて記述する
ベースとなる controller も作成しておく

// app/controllers/api/v1/v1.go
package controllers

import (
        "github.com/revel/revel"
        "github.com/shiro16/golang-bbs/app/utils"
        "net/http"
)

// 埋め込みによって revel.Controller をラップした ApiV1Controller を定義する
type ApiV1Controller struct {
        *revel.Controller
}

// エラーの際に返す Json 用の構造体
type ErrorResponse struct {
        Code    int    `json:"code"`
        Message string `json:"message"`
}

// 正常な際に返す Json 用の構造体(今回は1種類で統一する)
type Response struct {
        Results interface{} `json:"results"`
}

// 引数として渡されて interface にリクエストの Json の値を格納する
func (c *ApiV1Controller) BindParams(s interface{}) error {
        return utils.JsonDecode(c.Request.Body, s)
}

// Bad Request Error を返すやつ
func (c *ApiV1Controller) HandleBadRequestError(s string) revel.Result {
        c.Response.Status = http.StatusBadRequest
        r := ErrorResponse{c.Response.Status, s}
        return c.RenderJson(r)
}

// Not Found Error を返すやつ
func (c *ApiV1Controller) HandleNotFoundError(s string) revel.Result {
        c.Response.Status = http.StatusNotFound
        r := ErrorResponse{c.Response.Status, s}
        return c.RenderJson(r)
}

// Internal Server Error を返すやつ
func (c *ApiV1Controller) HandleInternalServerError(s string) revel.Result {
        c.Response.Status = http.StatusInternalServerError
        r := ErrorResponse{c.Response.Status, s}
        return c.RenderJson(r)
}

これでベースとなる処理が完了したので comments controller を作成していく

// app/controllers/api/v1/comments.go
package controllers

import (
        "github.com/revel/revel"
        "github.com/shiro16/golang-bbs/app/controllers"
)

type ApiV1Comments struct {
        ApiV1Controller
}

func (c ApiV1Comments) Index() revel.Result {
        r := Response{"index"}
        return c.RenderJson(r)
}

func (c ApiV1Comments) Show(id int) revel.Result {
        r := Response{"show"}
        return c.RenderJson(r)
}

func (c ApiV1Comments) Create() revel.Result {
        r := Response{"create"}
        return c.RenderJson(r)
}

func (c ApiV1Comments) Delete(id int) revel.Result {
        r := Response{"delete"}
        return c.RenderJson(r)
}

これで controller の作成は完了。
DB へ接続してデータを取得する等は次で行います。
実際に起動してみると下記のようになるかと。

$ revel run github.com/shiro16/golang-bbs

# 別窓等で
$ curl http://localhost:9000/api/v1/comments
{
  "results": "index"
}
$ curl http://localhost:9000/api/v1/comments/1
{
  "results": "show"
}
$ curl -X POST http://localhost:9000/api/v1/comments
{
  "results": "create"
}
$ curl -X DELETE http://localhost:9000/api/v1/comments/1
{
  "results": "delete"
}

DB 周りの処理を作成する

現状 ORM が revel には無い為自分で好きなものを使う。
revel の samples では gorp が使われているが
今回は gorm を使用する。

validation には validator を使用する。

DB の接続情報を追加

golang_bbs_development」という名前の DB 名にしています。

// conf/app.conf
[dev]
mode.dev = true
results.pretty = true
watch = true
watcher.mode = "normal"
log.trace.output = off
log.info.output  = stderr
log.warn.output  = stderr
log.error.output = stderr
db.info = "root@/golang_bbs_development?charset=utf8&parseTime=True
model を作成する

gorm を使用するので取得する
validator も取得しておく

$ go get github.com/jinzhu/gorm
$ go get gopkg.in/validator.v2

comment model を作成

// app/models/comment.go 
package models

import (
        "time"
)

type Comment struct {
        ID        uint64     `gorm:"primary_key" json:"id"`
        Nickname  string     `sql:"size:64" json:"nickname" validate:"max=64"`
        Body      string     `sql:"size:255" json:"body" validate:"min=1,max=255"`
        CreatedAt time.Time  `json:"created_at"`
        UpdatedAt time.Time  `json:"updated_at"`
        DeletedAt *time.Time `json:"deleted_at"`
}

DB への接続などの初期化処理を作成

// app/controllers/gorm.go
package controllers

import (
        _ "github.com/go-sql-driver/mysql"
        "github.com/jinzhu/gorm"
        "github.com/revel/revel"
        "github.com/shiro16/golang-bbs/app/models"
        "log"
)

var DB *gorm.DB

func InitDB() {
        db, err := gorm.Open("mysql", dbInfoString())
        if err != nil {
                log.Panicf("Failed to connect to database: %v\n", err)
        }

        db.DB()
        db.AutoMigrate(&models.Comment{}) # ここで table の作成を行っている
        DB = &db
}

func dbInfoString() string {
        s, b := revel.Config.String("db.info")
        if !b {
                log.Panicf("database info not found")
        }

        return s
}

上記を呼び出す処理を追記

// app/init.go
- import "github.com/revel/revel"
+ import(
+         "github.com/revel/revel"
+         "github.com/shiro16/golang-bbs/app/controllers"
+ )

func init() {
....
+ revel.OnAppStart(controllers.InitDB) // 28行目くらいに
}

DB 周りの処理の作成が完了し、
残すは作成した model を controller で実際に使用する処理を残すのみ

controller で model を使用する

comments controller を編集していく

package controllers

import (
	"github.com/revel/revel"
+ 	"github.com/shiro16/golang-bbs/app/controllers"
+ 	"github.com/shiro16/golang-bbs/app/models"
+ 	"gopkg.in/validator.v2"
)

type ApiV1Comments struct {
	ApiV1Controller
}

func (c ApiV1Comments) Index() revel.Result {
+ 	comments := []models.Comment{}

+ 	if err := controllers.DB.Order("id desc").Find(&comments).Error; err != nil {
+ 		return c.HandleInternalServerError("Record Find Failure")
+ 	}

+ 	r := Response{comments}
- 	r := Response{"index"}
	return c.RenderJson(r)
}

func (c ApiV1Comments) Show(id int) revel.Result {
+ 	comment := &models.Comment{}

+ 	if err := controllers.DB.First(&comment, id).Error; err != nil {
+ 		return c.HandleNotFoundError(err.Error())
+ 	}

+ 	r := Response{comment}
- 	r := Response{"show"}
	return c.RenderJson(r)
}

func (c ApiV1Comments) Create() revel.Result {
+ 	comment := &models.Comment{}

+ 	if err := c.BindParams(comment); err != nil {
+ 		return c.HandleBadRequestError(err.Error())
+ 	}

+ 	if err := validator.Validate(comment); err != nil {
+ 		return c.HandleBadRequestError(err.Error())
+ 	}

+ 	if err := controllers.DB.Create(comment).Error; err != nil {
+ 		return c.HandleInternalServerError("Record Create Failure")
+ 	}

+ 	r := Response{comment}
- 	r := Response{"create"}
	return c.RenderJson(r)
}

func (c ApiV1Comments) Delete(id int) revel.Result {
+ 	comment := models.Comment{}

+ 	if err := controllers.DB.First(&comment, id).Error; err != nil {
+ 		return c.HandleNotFoundError(err.Error())
+ 	}

+ 	if err := controllers.DB.Delete(&comment).Error; err != nil {
+ 		return c.HandleInternalServerError("Record Delete Failure")
+ 	}

+ 	r := Response{"success"}
- 	r := Response{"delete"}
	return c.RenderJson(r)
}

これで各 endpoint にアクセスしてみる

$ revel run github.com/shiro16/golang-bbs
# 別窓等で
$ curl -H "Content-type: application/json" -X POST -d '{"nickname":"shiro16", "body":"test comment"}' http://localhost:9000/api/v1/comments
{
  "results": {
    "id": 1,
    "nickname": "shiro16",
    "body": "test comment",
    "created_at": "2015-08-13T21:20:46.681910871+09:00",
    "updated_at": "2015-08-13T21:20:46.681910871+09:00",
    "deleted_at": null
  }
}
# validation がちゃんと機能しているかチェック
$ curl -H "Content-type: application/json" -X POST -d '{"nickname":"shiro16", "body":""}' http://localhost:9000/api/v1/comments
{
  "code": 400,
  "message": "Body: less than min"
}
$ curl http://localhost:9000/api/v1/comments
{
  "results": [
    {
      "id": 1,
      "nickname": "shiro16",
      "body": "test comment",
      "created_at": "2015-08-13T12:20:47Z",
      "updated_at": "2015-08-13T12:20:47Z",
      "deleted_at": null
    }
  ]
}
$ curl http://localhost:9000/api/v1/comments/1
{
  "results": {
    "id": 1,
    "nickname": "shiro16",
    "body": "test comment",
    "created_at": "2015-08-13T12:20:47Z",
    "updated_at": "2015-08-13T12:20:47Z",
    "deleted_at": null
  }
}
$ curl http://localhost:9000/api/v1/comments/2 
{
  "code": 404,
  "message": "record not found"
}
$ curl -X DELETE http://localhost:9000/api/v1/comments/1
{
  "results": "success"
}

まとめ

こんな感じで雑な部分もありますが revel を使って基本的な処理の作成が完了しました。
validation に関しては model にてチェックを行った方がいいかと思いますが、
今回は時間の関係で controller にて行っています。
今回作成したものはこちらで公開しています。
API 以外の処理も追加していますので参考にしてください。

今回の説明以外の詳しい内容は下記を参考にするといいと思います。
とくに sample を配布しているので「go get github.com/revel/samples」で取得して
見てみるといいかと思います。
Welcome to Revel, the Web Framework for Go!