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

  • 182
    いいね
  • 6
    コメント
この記事は最終更新日から1年以上が経過しています。

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