GolangのWebアプリケーションフレームワークBeegoを試す
Node.jsとかPhalconは速いんですけど、結局ネイティブでどこまで処理するかってところで、アプリケーションが大きくなってスクリプトで処理する部分が増えると、それなりに遅くなってしまいます。
そこで、Golangです。すべてネイティブなので、アプリケーションが大きくなっても速いままです!
Java VMでもいいんですが、アプリケーションサーバーを立ち上げてListenするだけで数百メガのメモリを消費してしまうので、クソアプリを気軽にサーバーに置けないのが悩ましいです。
GolangはListenするだけなら数メガで済みます!
HTTPサーバーやテンプレートのライブラリがデフォルトで組み込まれていて、簡単にWebアプリを作れそうですが、Webアプリ用のフレームワークがあったので、試しつつ掲示板を作ってみます。
martiniやRevelが有名どころらしいですが、ORマッパーまでサポートしているBeegoというフレームワークがあり、v1.3のStableらしいので、これを試してみます。
準備
Golangはすでにインストールしていることにします。
まずはBeegoをインストールします。
$ go get github.com/astaxie/beego
Beeツールもインストールします。
これでbeeコマンドが使えるようになります。
$ go get github.com/beego/bee
プロジェクトの作成
GOPATHのsrcに移動してプロジェクトを作成します。
$ cd $GOPATH/src
$ bee new beego-bbs
作成したディレクトリに移動して実行します。
$ cd beego-bbs
$ bee run
MySQLを使う
まずはMySQLドライバをインストールします。
$ go get github.com/go-sql-driver/mysql
/conf/app.confに接続情報を書きます。
mysqluser = "root"
mysqlpass = "password"
mysqlurls = "127.0.0.1”
mysqldb = "beego-bbs"
main.goのinit関数でMySQLに接続します。
// main.go
package main
import (
"fmt"
"time"
"strings"
"github.com/astaxie/beego"
"github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql"
_ "beego-bbs/routers"
)
func init() {
user := beego.AppConfig.String("mysqluser")
pass := beego.AppConfig.String("mysqlpass")
host := beego.AppConfig.String("mysqlurls")
db := beego.AppConfig.String("mysqldb")
datasource := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=utf8", user, pass, host, db)
orm.RegisterDataBase("default", "mysql", datasource, 30)
err := orm.RunSyncdb("default", false, true)
if err != nil {
panic(err)
}
beego.AddFuncMap("dateformatJst", func(in time.Time) string {
in = in.Add(time.Duration(9) * time.Hour)
return in.Format("2006-01-02 15:04:05")
})
beego.AddFuncMap("nl2br", func(in string) string {
return strings.Replace(in, "\n", "<br>", -1)
})
}
func main() {
beego.Run()
}
これでMySQLに接続できるようになりました!
Modelの作成
beegoではModelを/modelsの下に置くようです。
明示的なフィールドを持ったEntityに振る舞いを書くと見通しが悪くなるので、modelsの下にpostパッケージを切り、EntityとRepositoryを作成します。
Playのドキュメントではこういうのはアンチパターンと言ってますが、モデルオブジェクトって何でしょう?Service層もモデルですよね?
Entity
// models/post/post.go
package post
import (
"time"
)
type Post struct {
Id int64
Content string `orm:"type(text)"`
Created time.Time
Modified time.Time
}
次のRepositoryのinit関数で実装しますが、
beegoではorm.RegisterModel(new(Post))とするとテーブルが無かった場合に作成してくれるようで、stringはvarchar(255)になります。
また、それぞれのメンバにパラメータを設定することで、DBのデータ型を指定できるようです。
Contentはtextにしたいので、orm:”type(text)”
のパラメータを設定しました。
Repository
RepositoryではとりあえずDBに対するCRUD操作を実装します。
// models/post/post_repository.go
package post
import (
"time"
"github.com/astaxie/beego/orm"
)
type PostRepository struct {
}
func init() {
orm.RegisterModel(new(Post))
}
func (this *PostRepository) FindAll() ([]*Post, error) {
o := orm.NewOrm()
var posts []*Post
_, err := o.QueryTable("post").OrderBy("-Created").All(&posts)
return posts, err
}
func (this *PostRepository) FindById(id int64) (*Post, error) {
o := orm.NewOrm()
post := Post{Id: id}
err := o.Read(&post)
return &post, err
}
func (this *PostRepository) Save(p *Post) error {
var err error
o := orm.NewOrm()
now := time.Now()
if p.Id != 0 {
err = o.Read(p)
if err != nil {
p.Modified = now
_, err = o.Update(p)
}
} else {
p.Created = now
p.Modified = now
_, err = o.Insert(p)
}
return err
}
func (this *PostRepository) Delete(p *Post) error {
o := orm.NewOrm()
_, err := o.Delete(p)
return err
}
Controllerの作成
Controllerはcontrollersディレクトリに置きます。
PostControllerの構造体ではbeego.Controllerを匿名フィールドで宣言して継承します。
// controllers/post_controller.go
package controllers
import (
"beego-bbs/models/post"
"github.com/astaxie/beego"
)
type PostController struct {
beego.Controller
repository post.PostRepository
}
func (this *PostController) Index() {
posts, _ := this.repository.FindAll()
this.Data["posts"] = posts
this.Layout = "layouts/application.html"
this.TplNames = "post/index.html"
}
func (this *PostController) Create() {
post := post.Post{
Content: this.GetString("content"),
}
err := this.repository.Save(&post)
flash := beego.NewFlash()
if err != nil {
flash.Error("The post could not be saved. Please, try again.")
} else {
flash.Notice("The post has been saved.")
}
flash.Store(&this.Controller)
this.Redirect("/", 302)
}
Routing
beegoのrooterはいろいろ機能があるようで、Controllerとの棲み分けがいまいちよくわからないです。
とりあえずは一般的なフレームワークのrooterとして使います。
トップページは投稿一覧にします。
AutoRouterメソッドを使うとコントローラのアクションを自動的にルーティングしてくれるようです。
今回はIndexとCreateアクションを実装したので、それぞれ/post/index、/post/createにルーティングされます。
// routers/router.go
package routers
import (
"beego-bbs/controllers"
"github.com/astaxie/beego"
)
func init() {
beego.Router("/", &controllers.PostController{}, "*:Index")
beego.AutoRouter(&controllers.PostController{})
}
Viewのレイアウト
beegoではレイアウトファイルを定義でき、その中で{{.LayoutContent}}とすると各Viewが読み込まれます。
/views/layouts/application.htmlを作成します。
レイアウトファイルの指定はControllerでやってるthis.Layout = “layouts/application.html”です。
<!DOCTYPE html>
<html>
<head>
<title>{{.title}}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="/static/css/bootstrap.min.css">
<script src="/static/js/jquery-1.11.1.min.js" type="text/javascript" charset="utf-8"></script>
<script src="/static/js/bootstrap.min.js" type="text/javascript" charset="utf-8"></script>
{{range .moreStyles}}
<link rel="stylesheet" type="text/css" href="/static/{{.}}">
{{end}}
{{range .moreScripts}}
<script src="/public/{{.}}" type="text/javascript" charset="utf-8"></script>
{{end}}
</head>
<body>
<div class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/">Beego BBS</a>
</div>
</div>
</div>
<div class="container">
{{template "flash.html" .}}
<div class="content">
{{.LayoutContent}}
</div>
</div>
</body>
</html>
Bootstrapのcssなどを設定しました。
staticファイルはデフォルトでは/static以下に置くようです。
View
createdとかのtime.TimeはUTCで保存されるのですが、ロードすると時刻はそのままに+0900 JSTが付加される謎な動作をするので、テンプレート関数で補正しています。(main.goのinitに実装)
<!— views/post/index.html —>
{{range $post := .posts}}
<h4>名無しさん@Beego {{$post.Created|dateformatJst}}</h4>
{{$post.Content|htmlquote|nl2br|str2html}}
<hr />
{{end}}
<div class="col-md-6">
<form action="/post/create" method="post" accept-charset="utf-8">
<div class="form-group">
<label>Content</label>
<textarea name="content" class="form-control" required="required">
</textarea>
</div>
<input class="btn btn-primary" type="submit" value="Post”/>
</form>
</div>
いけましたね!
ソースコードをGithubに置きました。
※参考URL
http://beego.me/
http://d.hatena.ne.jp/yuheiomori0718/20140102/1388664225