Edited at

GoでCRUDでMVCなWEBアプリケーションを書く

More than 3 years have passed since last update.

Goの勉強を兼ねて、基本的なCURDを満たす、MVCなWEBアプリケーションを作りたいと思います。


データベース

DBはSQLiteを使ってるサンプルが多いですが、せっかくなので今回はMySQL5.6を使うことにします。


ライブラリ

GoでMVCフレームワークを作る場合、gojigormあたりが定番のようなので、これらを使います。

バリデーションだとこれが定番、というのが見つからなかったので、今回は使いやすそうな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.htmlnew.htmledit.htmlを作成します。

uesrモデルがマッピングするテーブルはgormmigrate機能で生成するので、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用のモデルを作ります。


user.go

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メソッドを追加します。


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)
}


すべて合わせるとこのようなモデルとなります。


user.go

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)
}


モデルができたらとりあえずテーブルを作りたいので、マイグレーションファイルを作成します。


db/migrate.go

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.CreateTableusersテーブルを作成します。

マイグレーションを実行します。

$ 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は初期化とルーティングを担当します。


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にアクションを用意します。

アクションの命名規約はモデル名+アクション名とします。


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にルーティング情報を記述します。


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にベーシック認証用のSuperSecurepleaseAuth、ベーシック認証用のIDとパスワードをconst Passwordに持たせます。


user_controller.go

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のルーティングにベーシック認証判定を追加します。


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")
}


コントローラーのアクションに処理を記述します。


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"
)

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で簡単なビューを作成します。


index.html

<html>

<p>user index</p>
{{range .}}
<a href="/user/edit/{{.Id}}">{{.Name}}</a><br/>
{{end}}
<a href="/user/new">new</a>
</html>


new.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>


exit.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と入力してサイトにアクセスします。

index.png

バリデーションも動いています。

new.png

ターミナルにアクセスログが吐かれます。

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