LoginSignup
236
218

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-11-15

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

236
218
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
236
218