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

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

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

# ライブラリ
GoでMVCフレームワークを作る場合、[goji](https://goji.io/)、[gorm](https://github.com/jinzhu/gorm)あたりが定番のようなので、これらを使います。
バリデーションだとこれが定番、というのが見つからなかったので、今回は使いやすそうな[valval](https://github.com/wcl48/valval)にしました。

# ライブラリのインストール
必要なライブラリを入れます。

```shell-session
$ 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`とします。

# ディレクトリ/ファイル構成
これまでの構成をまとめると、このようになります。

```shell-session
.
├── db
│   └── migrate.go
├── main.go
├── models
│   └── user.go
├── user_controller.go
└── view
    └── user
        ├── edit.html
        ├── index.html
        └── new.html
```
※modelsはpackageとして読み込むため、実際は$GOPATH配下に移動します。

# モデル
gorm用のモデルを作ります。

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

```go: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)
}
```

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

```go: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)
}

```

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

```go: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.CreateTable`で`users`テーブルを作成します。

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

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

```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`にアクションを用意します。
アクションの命名規約は`モデル名＋アクション名`とします。

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

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

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

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

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

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

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:index.html
<html>
<p>user index</p>
{{range .}}
<a href="/user/edit/{{.Id}}">{{.Name}}</a><br/>
{{end}}
<a href="/user/new">new</a>
</html>
```

```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>
```

```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>
```

# 実行

出来たら実際に実行してみます。

```shell-session
$ go run main.go user_controller.go
```

ターミナルでrunコマンドを実行すると、Gojiのログが表示されます。

```shell-session
2014/11/13 10:57:47.892536 Starting Goji on [::]:8000
```

ためしにブラウザで`http://127.0.0.1:8000/user/index`にアクセスしましょう。
ベーシック認証でIDとパスワードを聞かれるので、user/userと入力してサイトにアクセスします。

![index.png](https://qiita-image-store.s3.amazonaws.com/0/32430/3f55775b-20c3-681c-85a2-f95cdde19764.png)

バリデーションも動いています。
![new.png](https://qiita-image-store.s3.amazonaws.com/0/32430/b429702d-11be-bda4-d976-2dbae59493a8.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