Goの勉強を兼ねて、基本的なCURDを満たす、MVCなWEBアプリケーションを作りたいと思います。
データベース
DBはSQLiteを使ってるサンプルが多いですが、せっかくなので今回はMySQL5.6を使うことにします。
ライブラリ
GoでMVCフレームワークを作る場合、goji、gormあたりが定番のようなので、これらを使います。
バリデーションだとこれが定番、というのが見つからなかったので、今回は使いやすそうなvalvalにしました。
ライブラリのインストール
必要なライブラリを入れます。
$ go get github.com/wcl48/valval
$ go get github.com/jinzhu/gorm
$ go get github.com/zenazn/goji
$ go get github.com/go-sql-driver/mysql
どんな感じのWEBアプリケーションを作るか
お勉強なので、複雑なことはしません。
userモデルを作り、コントローラーはモデルを登録、修正、削除、インデックス表示するアクションだけを持たせることにします。
コントローラーの機能は下記のとおり。
index リスト表示
new 登録
edit 修正
delete 削除
よくあるWAFっぽくコントローラーとルーティングは分離したいので、main.go
はルーティングおよび初期処理の記述を行い、コントローラーはモデル名_controller.go
という名前とし、runおよびbuild時にまとめて呼び出すことにします。
また、ビューに関してはview/モデル名
をユーザーモデルのビューとして、その中にindex.html
、new.html
、edit.html
を作成します。
uesr
モデルがマッピングするテーブルはgorm
のmigrate
機能で生成するので、migrateファイル用のディレクトリを別途用意、db
とします。
ディレクトリ/ファイル構成
これまでの構成をまとめると、このようになります。
.
├── db
│ └── migrate.go
├── main.go
├── models
│ └── user.go
├── user_controller.go
└── view
└── user
├── edit.html
├── index.html
└── new.html
※modelsはpackageとして読み込むため、実際は$GOPATH配下に移動します。
モデル
gorm用のモデルを作ります。
package models
import(
"time"
"regexp"
)
type User struct {
Id int64
Name string `sql:"size:255"`
CreatedAt time.Time
UpdatedAT time.Time
DeletedAt time.Time
}
今回はユニークIDとNameのみを持つ簡易なモデルとしました。
またgormはDeletedAt
を付けると自動的にsoft deleteしてくれる仕様なので、DeletedAt
を指定しておきます。
さらにバリデーション制約もモデル側に持たせたいため、UserValidate
メソッドを追加します。
func UserValidate(user User)(error){
Validator := valval.Object(valval.M{
"Name": valval.String(
valval.MaxLength(20),
valval.Regexp(regexp.MustCompile(`^[a-z ]+$`)),
),
})
return Validator.Validate(user)
}
すべて合わせるとこのようなモデルとなります。
package models
import(
"github.com/wcl48/valval"
"time"
"regexp"
)
type User struct {
Id int64
Name string `sql:"size:255"`
CreatedAt time.Time
UpdatedAT time.Time
DeletedAt time.Time
}
func UserValidate(user User)(error){
Validator := valval.Object(valval.M{
"Name": valval.String(
valval.MaxLength(20),
valval.Regexp(regexp.MustCompile(`^[a-z ]+$`)),
),
})
return Validator.Validate(user)
}
モデルができたらとりあえずテーブルを作りたいので、マイグレーションファイルを作成します。
package main
import (
"github.com/jinzhu/gorm"
_ "github.com/go-sql-driver/mysql"
"models"
)
func main(){
db, _ := gorm.Open("mysql", "root:@/gorm?charset=utf8&parseTime=True")
db.CreateTable(&models.User{})
}
gorm.Open
でテーブルに接続し、db.CreateTable
でusers
テーブルを作成します。
マイグレーションを実行します。
$ go run db/migrate.go
usersテーブルが出来ました。
mysql> desc users;
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| deleted_at | datetime | YES | | NULL | |
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_a_t | datetime | YES | | NULL | |
+-------------+--------------+------+-----+---------+----------------+
5 rows in set (0.04 sec)
コントローラー
続いてコントローラーの作成に入ります。前述のようにmain.go
はルーティングと初期化のみを受け持ち、実際のアクションはuser_controller.go
に記述します。
main.go
は初期化とルーティングを担当します。
package main
import (
"github.com/zenazn/goji"
"github.com/zenazn/goji/web"
"github.com/zenazn/goji/web/middleware"
_ "github.com/go-sql-driver/mysql"
)
var db *gorm.DB
// main()はルーティング情報だけ記述する
func main() {
goji.Serve()
}
// ここは初期化処理専用
func init(){
// 初期化時にDBと接続
db, _ = gorm.Open("mysql", "root@/gorm?charset=utf8&parseTime=True")
}
user_controller.go
にアクションを用意します。
アクションの命名規約はモデル名+アクション名
とします。
package main
import (
"github.com/zenazn/goji/web"
"github.com/wcl48/valval"
"net/http"
"encoding/base64"
"strings"
"html/template"
"models"
"fmt"
"strconv"
)
func UserIndex(c web.C, w http.ResponseWriter, r *http.Request) {
}
func UserNew(c web.C, w http.ResponseWriter, r *http.Request){
}
func UserCreate(c web.C, w http.ResponseWriter, r *http.Request){
}
func UserEdit(c web.C, w http.ResponseWriter, r *http.Request){
}
func UserUpdate(c web.C, w http.ResponseWriter, r *http.Request){
}
func UserDelete(c web.C, w http.ResponseWriter, r *http.Request){
}
アクションができたので、main.go
にルーティング情報を記述します。
package main
import (
"github.com/zenazn/goji"
"github.com/jinzhu/gorm"
"github.com/zenazn/goji/web"
"github.com/zenazn/goji/web/middleware"
_ "github.com/go-sql-driver/mysql"
)
var db gorm.DB
func main() {
user := web.New()
goji.Handle("/user/*", user)
user.Use(middleware.SubRouter)
user.Get("/index", UserIndex)
user.Get("/new", UserNew)
user.Post("/new", UserCreate)
user.Get("/edit/:id", UserEdit)
user.Post("/update/:id", UserUpdate)
user.Get("/delete/:id", UserDelete)
goji.Serve()
}
func init(){
db, _ = gorm.Open("mysql", "root@/gorm?charset=utf8&parseTime=True")
}
一応セキュリティを気にして、goji
のサンプルを真似てベーシック認証をつけておきます。
user_controller.go
にベーシック認証用のSuperSecure
とpleaseAuth
、ベーシック認証用のIDとパスワードをconst Password
に持たせます。
package main
// ベーシック認証のIDとパスワード
const Password = "user:user"
import (
"github.com/zenazn/goji/web"
"github.com/wcl48/valval"
"net/http"
"encoding/base64"
"strings"
"html/template"
"models"
"fmt"
"strconv"
)
func UserIndex(c web.C, w http.ResponseWriter, r *http.Request) {
}
func UserNew(c web.C, w http.ResponseWriter, r *http.Request){
}
func UserCreate(c web.C, w http.ResponseWriter, r *http.Request){
}
func UserEdit(c web.C, w http.ResponseWriter, r *http.Request){
}
func UserUpdate(c web.C, w http.ResponseWriter, r *http.Request){
}
func UserDelete(c web.C, w http.ResponseWriter, r *http.Request){
}
// ベーシック認証する処理
func SuperSecure(c *web.C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Basic ") {
pleaseAuth(w)
return
}
password, err := base64.StdEncoding.DecodeString(auth[6:])
if err != nil || string(password) != Password {
pleaseAuth(w)
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// Authヘッダを受け付けるための処理
func pleaseAuth(w http.ResponseWriter) {
w.Header().Set("WWW-Authenticate", `Basic realm="Gritter"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Go away!\n"))
}
main.goのルーティングにベーシック認証判定を追加します。
package main
import (
"github.com/zenazn/goji"
"github.com/jinzhu/gorm"
"github.com/zenazn/goji/web"
"github.com/zenazn/goji/web/middleware"
_ "github.com/go-sql-driver/mysql"
)
var db gorm.DB
func main() {
user := web.New()
goji.Handle("/user/*", user)
user.Use(middleware.SubRouter)
user.Use(SuperSecure) // ベーシック認証処理追加
user.Get("/index", UserIndex)
user.Get("/new", UserNew)
user.Post("/new", UserCreate)
user.Get("/edit/:id", UserEdit)
user.Post("/update/:id", UserUpdate)
user.Get("/delete/:id", UserDelete)
goji.Serve()
}
func init(){
db, _ = gorm.Open("mysql", "root@/gorm?charset=utf8&parseTime=True")
}
コントローラーのアクションに処理を記述します。
package main
import (
"github.com/zenazn/goji/web"
"github.com/wcl48/valval"
"net/http"
"encoding/base64"
"strings"
"html/template"
"models"
"fmt"
"strconv"
)
var tpl *template.Template
const Password = "user:user"
type FormData struct{
User models.User
Mess string
}
func UserIndex(c web.C, w http.ResponseWriter, r *http.Request) {
Users := [] models.User{}
db.Find(&Users)
tpl = template.Must(template.ParseFiles("view/user/index.html"))
tpl.Execute(w,Users)
}
func UserNew(c web.C, w http.ResponseWriter, r *http.Request){
tpl = template.Must(template.ParseFiles("view/user/new.html"))
tpl.Execute(w,FormData{models.User{}, ""})
}
func UserCreate(c web.C, w http.ResponseWriter, r *http.Request){
User := models.User{Name: r.FormValue("Name")}
if err := models.UserValidate(User); err != nil {
var Mess string
errs := valval.Errors(err)
for _, errInfo := range errs {
Mess += fmt.Sprint(errInfo.Error)
}
tpl = template.Must(template.ParseFiles("view/user/new.html"))
tpl.Execute(w,FormData{User, Mess})
} else {
db.Create(&User)
http.Redirect(w, r, "/user/index", 301)
}
}
func UserEdit(c web.C, w http.ResponseWriter, r *http.Request){
User := models.User{}
User.Id, _ = strconv.ParseInt(c.URLParams["id"], 10, 64)
db.Find(&User)
tpl = template.Must(template.ParseFiles("view/user/edit.html"))
tpl.Execute(w,FormData{User, ""})
}
func UserUpdate(c web.C, w http.ResponseWriter, r *http.Request){
User := models.User{}
User.Id, _ = strconv.ParseInt(c.URLParams["id"], 10, 64)
db.Find(&User)
User.Name = r.FormValue("Name")
if err := models.UserValidate(User); err != nil {
var Mess string
errs := valval.Errors(err)
for _, errInfo := range errs {
Mess += fmt.Sprint(errInfo.Error)
}
tpl = template.Must(template.ParseFiles("view/user/edit.html"))
tpl.Execute(w,FormData{User, Mess})
} else {
db.Save(&User)
http.Redirect(w, r, "/user/index", 301)
}
}
func UserDelete(c web.C, w http.ResponseWriter, r *http.Request){
User := models.User{}
User.Id, _ = strconv.ParseInt(c.URLParams["id"], 10, 64)
db.Delete(&User)
http.Redirect(w, r, "/user/index", 301)
}
func SuperSecure(c *web.C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Basic ") {
pleaseAuth(w)
return
}
password, err := base64.StdEncoding.DecodeString(auth[6:])
if err != nil || string(password) != Password {
pleaseAuth(w)
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func pleaseAuth(w http.ResponseWriter) {
w.Header().Set("WWW-Authenticate", `Basic realm="Gritter"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Go away!\n"))
}
CreateとUpdateにはバリデーションを入れています。
バリデーションエラーを画面に表示するため、user
モデルとエラーメッセージを持つ構造体として、FormData
を使っています。
#ビュー
ビューはview/モデル名/アクション名.htmlで簡単なビューを作成します。
<html>
<p>user index</p>
{{range .}}
<a href="/user/edit/{{.Id}}">{{.Name}}</a><br/>
{{end}}
<a href="/user/new">new</a>
</html>
<html>
<form method="post" action="/user/new" >
<label>
Name
<input type="text" name="Name" value="{{.User.Name}}"/>
</label>
<input type="submit" name="Submit" value="登録" /><br/>
{{.Mess}}
</form>
<a href="/user/index">back</a>
</html>
<html>
<form method="post" action="/user/update/{{.User.Id}}" >
<label>
Name
<input type="text" name="Name" value="{{.User.Name}}" />
</label>
<input type="submit" name="Submit" value="更新" /><br/>
<a href="/user/delete/{{.User.Id}}">delete</a>
</form>
{{.Mess}}<br/>
<a href="/user/index">back</a>
</html>
実行
出来たら実際に実行してみます。
$ go run main.go user_controller.go
ターミナルでrunコマンドを実行すると、Gojiのログが表示されます。
2014/11/13 10:57:47.892536 Starting Goji on [::]:8000
ためしにブラウザでhttp://127.0.0.1:8000/user/index
にアクセスしましょう。
ベーシック認証でIDとパスワードを聞かれるので、user/userと入力してサイトにアクセスします。
ターミナルにアクセスログが吐かれます。
2014/11/13 10:59:03.897613 [hogehoge/wMBC8y2tTL-000001] Started GET "/user/index" from 192.168.33.1:49617
2014/11/13 10:59:03.897696 [hogehoge/wMBC8y2tTL-000001] Returning 401 in 30.466us
2014/11/13 10:59:05.536546 [hogehoge/wMBC8y2tTL-000002] Started GET "/user/index" from 192.168.33.1:49617
2014/11/13 10:59:05.553565 [hogehoge/wMBC8y2tTL-000002] Returning 200 in 16.861414ms
バイナリ化
Goはバイナリ化してしまえばバイナリ単体で実行できますので、バイナリ化もしてみます。
$ go build main.go user_controller.go
生成されたmain
を実行します。
[hogehoge@unkoman goji]$ ./main
2014/11/14 09:59:41.280222 Starting Goji on [::]:8000
動きました。
まとめ
ざっと作ってみましたが、goji, gormを使えばそれほど苦労なくGoでMVCなWAFができそうなことがわかりました。
実際にサービスで使うことを考えた場合、設定をyamlで記述して、go-yaml
で読み込んで環境によって設定を変えたりとか、あとはgojiでコントローラーのテスト方法がわからなかったので、テストの記述が課題として残ります。
あとこの程度の開発だと、goの特徴であるgorutine
とかchannel
の出番が全く無いのが悲しい。
追記
設定をyamlで行うのと、テストの方法を書きました。
http://qiita.com/masahikoofjoyto/items/d381ee31854405c86c47