2014/10/22


昨日は naoina さんの genmai を使って wiki を書きましたが、今日は同じく naoina さんが開発している kocha を使って簡単な SPA アプリを書いてみたいと思います。

Kocha web application framework for Go

A convenient web application framework for Go

http://naoina.github.io/kocha/

巷の golang の WAF の殆どは、どちらかというと Sinatra 風の、自前でルーティングハンドラ兼コントローラを書いてくタイプの物ですが、kocha はどちらかというと rails 寄りの WAF に位置します。

rails g でコントローラを生成したりモデルを作ったりといった、コマンドラインベースのジェネレータを使ってガシガシと開発していくフローを実現出来ます。以下手順を追って説明します。

アプリケーションひな形を作る

$ kocha new github.com/mattn/kocho
    create directory /home/mattn/dev/godev/github.com/mattn/kocho/app/controller
              create /home/mattn/dev/godev/github.com/mattn/kocho/app/controller/root.go
    create directory /home/mattn/dev/godev/github.com/mattn/kocho/app/view/error
              create /home/mattn/dev/godev/github.com/mattn/kocho/app/view/error/404.html
              create /home/mattn/dev/godev/github.com/mattn/kocho/app/view/error/500.html
    create directory /home/mattn/dev/godev/github.com/mattn/kocho/app/view/layout
              create /home/mattn/dev/godev/github.com/mattn/kocho/app/view/layout/app.html
              create /home/mattn/dev/godev/github.com/mattn/kocho/app/view/root.html
    create directory /home/mattn/dev/godev/github.com/mattn/kocho/config
              create /home/mattn/dev/godev/github.com/mattn/kocho/config/app.go
              create /home/mattn/dev/godev/github.com/mattn/kocho/config/routes.go
              create /home/mattn/dev/godev/github.com/mattn/kocho/main.go
    create directory /home/mattn/dev/godev/github.com/mattn/kocho/public
              create /home/mattn/dev/godev/github.com/mattn/kocho/public/robots.txt

この状態でも kocha run とすれば動作します。この辺は rails に似た動作になっていますね。golang の場合はシングルバイナリがウリですがもちろん kocha でも kocha build とすればシングルバイナリが生成されます。

モデルを作る

このサンプルではユーザ登録画面を作ろうと思います。とは言っても属性は名前(name)だけとなります。

$ kocha g model user
              create app/model/users.go
生成されるコードは以下の通り。 package model

import (
    "github.com/naoina/genmai"
)

type User struct {
    Id int64 `db:"pk" json:"id"`

    genmai.TimeStamp
}

func (m *User) BeforeInsert() error {
    // FIXME: This method is auto-generated by Kocha.
    //        You can remove this method if unneeded.
    return m.TimeStamp.BeforeInsert()
}

func (m *User) AfterInsert() error {
    // FIXME: This method is auto-generated by Kocha.
    //        You can remove this method if unneeded.
    return nil
}

func (m *User) BeforeUpdate() error {
    // FIXME: This method is auto-generated by Kocha.
    //        You can remove this method if unneeded.
    return m.TimeStamp.BeforeUpdate()
}

func (m *User) AfterUpdate() error {
    // FIXME: This method is auto-generated by Kocha.
    //        You can remove this method if unneeded.
    return nil
}

func (m *User) BeforeDelete() error {
    // FIXME: This method is auto-generated by Kocha.
    //        You can remove this method if unneeded.
    return nil
}

func (m *User) AfterDelete() error {
    // FIXME: This method is auto-generated by Kocha.
    //        You can remove this method if unneeded.
    return nil
}

これに Name 属性を足します。

type User struct {
    Id int64 `db:"pk" json:"id"`
    Name string `json:"name"`

    genmai.TimeStamp
}

コントローラを作る

$ kocha g controller users
              create app/controller/users.go
              create app/view/users.html

コントローラのひな形が生成されました。生成されるコードは以下の様になっています。

package controller

import (
    "github.com/naoina/kocha"
)

type Users struct {
    *kocha.DefaultController
}

func (us *Users) GET(c *kocha.Context) kocha.Result {
    // FIXME: auto-generated by kocha
    return kocha.Render(c)
}

func (us *Users) POST(c *kocha.Context) kocha.Result {
    // FIXME: auto-generated by kocha
    return kocha.Render(c)
}
これを、昨日の例を参考に以下の様に修正します。POST は name パラメータを貰ってモデルに登録します。 package controller

import (
    "github.com/mattn/kocho/db"
    "github.com/mattn/kocho/app/model"
    "github.com/naoina/kocha"
)

type Users struct {
    *kocha.DefaultController
}

func (us *Users) GET(c *kocha.Context) kocha.Result {
    var users []model.User
    err := db.Get("default").Select(&users)
    if err != nil {
        return kocha.RenderError(c, 500, err)
    }
    return kocha.RenderJSON(c, users)
}

func (us *Users) POST(c *kocha.Context) kocha.Result {
    name := c.Params.Get("name")
    if name == "" {
        return kocha.RenderError(c, 400"Bad request")
    }
    user := &model.User{Name: name}
    _, err := db.Get("default").Insert(user)
    if err != nil {
        return kocha.RenderError(c, 500, err)
    }
    return kocha.RenderJSON(c, user)
}

ビューを作る

今回作るサンプルアプリケーションは SPA (Single Page Application) なので、ルートのページのみ編集します。

root.html の編集前は以下の様になっています。

{{define "content"}}
<h1>Welcome to Kocha</h1>
{{end}}

これを弄って以下の様にしました。

{{define "content"}}
<script>
$(function() {
    function update() {
        $.getJSON("/users"function(data) {
            var $u = $('#users').empty();
            $.each(data, function(index, user) {
                $('<li/>').text(user.name).appendTo($u)
            });
        })
    }
    $('#send').click(function() {
        $.post("/users"{"name": $('#name').val()}function() {
            $('#name').val('');
            update();
        });
    });
    update();
})
</script>
<h1 class="title">ユーザ一覧</h1>
<label for="name">名前</label>
<input id="name" type="text">
<input id="send" type="submit" value="追加">
<ul id="users"></ul>
{{end}}

jQuery を使うので app/view/layout/app.html も以下の様に修正しました。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
  <link rel="stylesheet" href="style.css" media="all">
  <title>ユーザ一覧</title>
</head>
<body>
  {{template "content" .}}
</body>
</html>

名前欄に入力された文字列を Ajax で POST して、成功すれば一覧を更新する単純なアプリケーションが出来ました。

kocho

まとめ

始めに仕組みを覚えるまでに間違ったルータを作ってしまったりモデルが決まらなかったりしますが、一旦作り方を覚えてしまえば rails 並みの開発効率が生み出せると思います。

実際にこのサンプルアプリケーションを作るのに20分程度でした(僕は kocha はずいぶん前から遊んでいたので使い方は大体知ってはいましたが)。

ちなみに kocha、Windows でも動きます!(ここ大事)

例によって作ったプリケーションを github に置いておきます。

mattn/kocho - GitHub
https://github.com/mattn/kocho
Posted at by



2014/10/21



Web アプリケーションを書くときは今までずっと

というスタンスを何故か貫いて来たんだけど、最近ようやく web.go をやめて goji を使う様になった。

Goji

A web microframework for Golang

https://goji.io/

理由としては

  • Sinatra ライクでありながら高度な正規表現マッチも使えるルータ
  • それでいて net/http コンパチ
  • 簡単に実装出来るミドルウェアスタック
  • グレースフルシャットダウン
  • そして何と言っても速い

ミドルウェアも一般的な Web アプリケーションを作るには十分な物が既に揃ってます

それでもやっぱり大きめな物は net/http で書くんだけど、小規模から中規模な物を一気にゴリゴリっと書く場合には goji は便利です。もちろん Windows でも動きます。

今日はこの goji と pongo2 というテンプレートエンジン、そして naoina さんが開発している ORM の genmai を使って Wiki を作ってみたいと思います。

ORM には genmai を使います。

おれのかんがえたさいきょうの ORM for Golang - 何気に大変
http://naoina.plog.la/2014/02/17/121743410109
naoina/genmai - GitHub

package main import ( "database/sql" "fmt" "time" _ "github.com/mattn/go-sqlite3" // _ "github.com/g...

https://github.com/naoina/genmai

genmai、とても便利です。フォルダ構成はルートにアプリケーション、view ディレクトリにテンプレート、assets に静的ファイルを置きます。テンプレートエンジンには pongo2 を使います。pongo2 は jedie でも使っています。

genmai を食す際の構造体は以下の通り。

type Page struct {
    Id        int64     `db:"pk"`
    Title     string    `db:"unique" json:"title"`
    Body      string    `json:"body"`
    URL       string    `db:"-"`
    Deleted   bool      `json:"deleted"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

SQLite3 のダイアレクトを指定して Page テーブルを作ってもらいます。

db, err := genmai.New(&genmai.SQLite3Dialect{}, "./wiki.db")
if err != nil {
    log.Fatalln(err)
}
if err := db.CreateTableIfNotExists(&Page{}); err != nil {
    log.Fatalln(err)
}

Wiki なのでルーティングは以下の通り。

  • 静的ファイル /assets, GET
  • ページ一覧 /, GET
  • ページ /wiki/:title, GET
  • 編集画面 /wiki/:title/edit, GET
  • 更新処理 /wiki/:title, POST

goji だと以下の様に登録します。

goji.Get("/assets/*", http.FileServer(http.Dir(".")))
goji.Get("/", showPages)
goji.Get("/wiki/:title", showPage)
goji.Get("/wiki/:title/edit", editPage)
goji.Post("/wiki/:title", postPage)

ページの表示処理は以下の様にしました。

func showPage(c web.C, w http.ResponseWriter, r *http.Request) {
    wiki := getWiki(c)
    db := wiki.DB

    var pages []Page
    err := db.Select(&pages, db.From(&Page{}), db.Where("title""=", c.URLParams["title"]))
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    var page Page
    if len(pages) > 0 {
        page = pages[0]
    }
    if page.Title == "" {
        page.Title = c.URLParams["title"]
    }
    page.URL = wiki.PageURL(&page)
    tpl, err := pongo2.DefaultSet.FromFile("page.tpl")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    tpl.ExecuteWriter(pongo2.Context{"page": page}, w)
}

そしてテンプレート view/page.tpl は以下の通り。pongo2 はテンプレートのキャッシュに対応しているので処理が書けたら起動したままテンプレートファイルを編集する事が出来てとても便利です。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ page.Title }}</title>
    <link rel="stylesheet" href="/assets/css/style.css" media="all">
</head>
<body>
    <div id="content">
        <h1 class="title">{{ page.Title }}</h1>
        <div class="body">
        {{ page.Body | markdown }}
        </div>
        <span class="date">Updated at: {{ page.UpdatedAt | date: "2006/01/02 03:04:05" }}</span>
        <br />
        <a href="{{ wiki.URL }}">Page index</a>
        <a href="{{ page.URL }}/edit">Edit this page</a>
    </div>
</body>
</html>

編集画面や登録処理も同様の手順で作ると、以下の様な Wiki ページが出来上がります。

y-u-no

ソースコードは以下に置いておきます。興味のある方はどうぞ。

mattn/goji-wiki - GitHub
https://github.com/mattn/goji-wiki
Posted at by



2014/09/26


先日、golang の開発リポジトリに generate が入りました。

Go generate: A Proposal

The go build command automates the construction of Go programs but sometimes preliminary processing is required, processing that go build does not support.

https://docs.google.com/document/d/1V03LUfjSADDooDMhe-_K59EgpTEm3V8uvQRuNMAEnjg/edit

皆さんが期待している様な物なのかそうでないのか分かりませんが、ひとまずこの提案書を見る限り

  • 使うのはライブラリユーザではなくライブラリ作者
  • go build で自動で generate してくれる機能はない
  • shell 的なワンライナーは実行出来ない

どちらかと言うと使用するのは開発時で、バイナリを同梱する目的で golang のソースを吐くまでの手順であったり、構造体に特殊なメソッドを生やしたりという目的で使われます。また yacc のソースから golang のソースを吐くなどといった用途も考えられます。生成される物が golang のソースに限られている訳でもありません。

この特殊なメソッドを吐き出す、と聞くとどうしても generics を思い浮かべる方が多いと思いますが、上記の通り「自動では生成されない」という制限がある事から期待されている使い方は現状出来ません。

今日はこの新しく入った generate を使って、どの様な効果が得られるのかを gen というツールを使って説明したいと思います。

まず以下のコード(food.go)を用意します。

//go:generate gen -force

package food

// +gen *
type Food struct {
    Name string
    Price int
}

gen というツールが +gen となっている部分を扱います。

clipperhouse/gen - GitHub

README.md What’s this? gen is a code-generation tool for Go. It’s intended to offer generics-like fu...

https://github.com/clipperhouse/gen

gen はこの識別が付いている type 宣言から便利な関数群を作ってくれます。

food.go があるフォルダで以下を実行します。

$ go generate

すると food_gen.go というコードが生成されます。少し大きすぎるので gist に貼りつけました。詳しくは gen の README を参照して頂きたいですが、配列を扱う上で便利な関数群が生成されます。

あとはこれを使って処理を書くのみとなります。

package main

import (
    "fmt"
    . "github.com/mattn/go-example/food"
)

func main() {
    foods := Foods{
        {"リンゴ"110},
        {"みかん"70},
        {"メロン"400},
    }

    foods.All(func(f *Food) bool {
        if f.Price < 200 {
            fmt.Println(f.Name)
        }
        return true
    })
}

この例は gen を使いましたが、go-assetsgo-bindata でバイナリからソースファイルを生成したり、msgp を使って構造体を MessagePack 対応したりといった用途にも使用出来るかと思います。

ライブラリユーザではなくライブラリ開発者にとっては便利な機能だと思いますね。

Posted at by