( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

"err"という文字列をHighlightしておくとGolangのコードリーディングが捗る

f:id:yuroyoro:20140812144144p:plain

vimの人はこんな感じで

autocmd FileType go :highlight goErr cterm=bold ctermfg=214
autocmd FileType go :match goErr /\<err\>/

golang勉強会で「cgoやってみた」という話をしてきた

Go lang勉強会 - connpassで発表してきた。
今までにblogに書いたcgoの話。

以下が資料とサンプルコード。

久しぶりに人前で発表した気がする。
当日はdemoもやるとなると時間が足りなくなるだろうという予想通りになり、ブランクを感じた。
あと、ネタ成分が不足気味だったのでリハビリしなければならない。

他の人の発表も面白かった。特にライセンスの話など。皆さんお疲れ様でした。

こんな勉強会なら、また参加したいものです。

ぼくのかんがえたさいきょうのGit Repository Browser: Gitterb をRuby2.1.2/Rails4にupgradeしてDockerImage作った話

3年ほど前に、GitterbというGitリポジトリのコミットログを可視化するツールを作った。


このアプリケーションはRuby1.9/Rails3.2 で書かれていて、今となってはもう動かないので、Ruby2.1/Rails4へupgradeした。

デモサイトはこちら http://gitterb.yuroyoro.net/


依存しているGritというRubyからGitリポジトリをホゲるGemがRuby2系では動かないので、libgit2のRubyバインディングであるRuggedに移行している。

あと、せっかくなのでCentOS7で動くDockerImageを作った。Docker Hubにおいてある。

以下のようにdocker pullした後にrunすると、port3000でサンプルが起動する。

docker pull yuroyoro/gitterb
docker run -d -p 3000:3000 -t yuroyoro/gitterb


macでboot2dcoker使ってる人は、port forwardingしてくだされ。

ssh -N -L 3000:127.0.0.1:3000 docker@localhost -p 2022

( ꒪⌓꒪) あばばばばばばばば

ʕ  ゚皿゚ ʔ GolangのASTを可視化するツールを作った

はじめてのGo Runtime。
ということで、GoのAST(抽象構文木)を可視化するツールを書いた。

yuroyoro/goast-viewer · GitHub

goast.yuroyoro.net にデモがある。

go/astパッケージを使うと、GoのソースコードからAST(抽象構文木)を得ることができる。
あとはこれをAngulerJSとか使ってみて可視化してみただけ。

f:id:yuroyoro:20140630220303p:plain

ソースコードをアップロードするか、入力してparseボタンを押すと、右側にASTが展開される。マウスオーバーするとASTのnodeに該当するコードが選択状態になる。

以下の手順でインストールできるます

$ go get -d github.com/yuroyoro/goast-viewer
$ cd $GOPATH/src/github.com/yuroyoro/goast-viewer
$ make install

GoよりAngulerJSの方が難しかったʕ  ゚皿゚ ʔ

オマエらはもっとObject#tryの便利さについて知るべき

  arr = [
    ["foo", "", "bar"], 
    nil,
  ].sample
  
  arr.try(:reject, &:blank?) #=> [“foo”, “bar”]
  • Object#tryはnil-safeなcallとして使える
  • blockを取るメソッドのsymbolを渡した場合に、第二引数にprocを渡しても動作する

activesupport.gem
tryと関数合成は本体に入れて欲しい

ʕ  ゚皿゚ ʔ GolangのWeb Application Frameworkを色々試してみてもいいかしら?

うちのメロンちゃんはLv.117です。

Golangで、簡単なWebアプリケーションをいくつかのフレームワークを用いて作成してみた。
サンプルアプリケーションは、こんな感じのPhotoギャラリーアプリケーションで、画像URLを入力すると追加される。
PureというCSSフレームワークのサンプルから拝借した。

f:id:yuroyoro:20140615172809p:plain


ソースコードGithubで公開している。

yuroyoro/golang_webapp_framework_samples · GitHub


今回試したのは、net/httpパッケージMartiniRevel の3つ。

net/http編

まずは基本のnet/http編。ソースコードこちら


net/httpパッケージでサーバーを書くのはとても簡単だ。以下のように、http.HandleFuncにpathのパターンと処理する関数を登録して、http.ListenAndServeで待ち受けるportを指定すればいい。

func main() {
	http.Handle("/css/layouts/", http.StripPrefix("/css/layouts/", http.FileServer(http.Dir("views/css/layouts"))))
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/save", saveHandler)
	http.ListenAndServe(":5050", nil)
}


http.HandleFuncに登録する関数は、このように http.ResponseWriterとhttp.Requestを受けるようにしておく。200 OK hello worldを返す関数はこんなん。

func indexHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)  // headerへ200 OKを設定
	io.WriteString(w, "hello, world!\n") // response bodyへwrite

html/template

Golangには、builtinのテンプレートエンジンとしてhtml/templateが提供されている。今回のサンプルアプリケーションでは、これを用いてレスポンスを生成している。以下は、"/"を処理するindexHandler関数。

type Body struct {
	First  models.Photo
	Photos []models.Photo
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
	page, err := strconv.Atoi(r.URL.Path[1:])
	if err != nil {
		page = 0
	}

  // databaseから登録されているphotoを読み出す
	body, err := loadBody(page)
	if err != nil {
		panic(err)
	}

  // html/templateでレスポンスを生成して http.ResponseWriterで書き出す
	indexTemplate.Execute(w, body)
}

func init() {
	indexTemplate = template.Must(template.ParseFiles("views/index.html"))
}


indexTemplateは、あらかじめinit関数で"views/index.html"というファイルを読み込んで生成してある。以下は、今回のサンプルで用いているものからの抜粋。

{{range $index, $photo := .Photos }}
    {{with $photo}}
        {{if eq $index 4 5 }}
          <div class="photo-box pure-u-1 pure-u-med-1-2 pure-u-lrg-2-3">
              <a href="{{.URL}}">
                  <img src="{{.URL}}" >
              </a>

              <aside class="photo-box-caption">
                <span>by <a href="https://twitter.com/{{.Author}}">@{{.Author}}</a></span>
              </aside>
          </div>
        {{else}}
          <div class="photo-box pure-u-1 pure-u-med-1-2 pure-u-lrg-1-3">
              <a href="{{.URL}}">
                  <img src="{{.URL}}" >
              </a>

              <aside class="photo-box-caption">
                <span>by <a href="https://twitter.com/{{.Author}}">@{{.Author}}</a></span>
              </aside>
          </div>
        {{end}}
    {{end}}
{{end}}


テンプレート内部では、{{ ... }} で値の展開や分岐や繰り返しを指定する。` {{range $index, $photo := .Photos }}` は、'indexTemplate.Execute(w, body)'で渡したBody構造体のメンバーであるPhotosを元に繰り返しを行う、という意味。

O/R全裸

Golangには、Databaseを扱うための標準的なAPIとしてsql - The Go Programming Languageが提供されている。今回のサンプルではsqltite3を使うので、go-sqlite3をdriverとして用いる。

O/R全裸としては、gorpがよいという話なので使ってみた。go-sqlite3, gorpともにgo getで入れておくこと。

go get github.com/mattn/go-sqlite3
go get github.com/coopernurse/gorp


gorpの使い方については、「Big Sky :: Go言語向けの ORM、gorp がなかなか良い」を見て貰うのが手っ取り早い。
今回のサンプルでは、"models/photos.go"にDatabaseへのアクセスを行う処理を切り出してある。

package models

import (
	"database/sql"
	"errors"
	"github.com/coopernurse/gorp"
	_ "github.com/mattn/go-sqlite3"
)

var DatabaseFile = "photos.db"

type Photo struct {
	Id     int64
	URL    string
	Author string
}

func InitDb() (*gorp.DbMap, error) {
	db, err := sql.Open("sqlite3", DatabaseFile)
	if err != nil {
		return nil, err
	}

	dbmap := &gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}}
	dbmap.AddTableWithName(Photo{}, "photos").SetKeys(true, "Id")

	err = dbmap.CreateTablesIfNotExists()
	if err != nil {
		return nil, err
	}

	return dbmap, nil
}

InitDb関数で、databaseをopenしている。Databaseのテーブルにmappingする構造体としてphotoを用意し、gop.DbMap構造体にAddTableWithName関数を用いてmappingを登録している。カラム名と構造体メンバのmappingは基本CoCだが、メンバにtagを指定することでカスタマイズできる。

func (p Photo) Save() error {
	dbmap, err := InitDb()
	if err != nil {
		return err
	}

	defer dbmap.Db.Close()


	// Insert
	dbmap.Insert(&p)
	if err != nil {
		return err
	}

	return nil
}

func LoadPhotos(page int) ([]Photo, error) {
	dbmap, err := InitDb()
	if err != nil {
		return nil, err
	}

	defer dbmap.Db.Close()

	if page < 0 {
		return nil, errors.New("invalid page number")
	}

	limit := 8
	offset := page * limit

	var photos []Photo

	_, err = dbmap.Select(&photos, "SELECT id, url, author FROM photos ORDER BY id DESC LIMIT ? OFFSET ?", limit, offset)
	if err != nil {
		return nil, err
	}

	return photos, nil
}

Databaseへのinsertはdbmap.Insertに構造体を渡すだけ。dbmap.SelectでSQLを渡すと、結果をmappingした構造体のスライスで取得できる。便利。

testing (builtin)

これで、サーバーとテンプレートとモデルがそろったので、アプリケーションとして動作する形になった。が、すこし寄り道して、テストについても書いておこうと思う。

Golang組み込みのテスティングフレームワークとして、testingパッケージが提供されている。これは本当に単なるテスティングフレームワークの元となるライブラリで、素のままで使うのはちと辛みがある。が、何事も基本から。

"models/photos.go"に対してのテストを"models/photos_test.go"に書く。

package models

import (
	"fmt"
	"testing"
)

func TestSave(t *testing.T) {
	DatabaseFile = "photos_test.db"
	dbmap, err := InitDb()

	if err != nil {
		t.FailNow()
	}

	defer dbmap.Db.Close()

	err = dbmap.TruncateTables()

	if err != nil {
		t.FailNow()
	}

	photo := Photo{
		URL:    "http://example.com",
		Author: "yuroyoro",
	}

	photo.Save()

	var photos []Photo

	_, err = dbmap.Select(&photos, "SELECT id, url, author FROM photos ORDER BY id ASC ")
	if err != nil {
		t.FailNow()
	}

	if len(photos) != 1 {
		t.Error("Photo.Save() failed")
	}

	if photos[0].URL != photo.URL || photos[0].Author != photo.Author {
		t.Error("Photo.Save() failed")
	}

}

go testコマンドで、"*_test.go"というファイル内でTestから始まる関数が実行される。assert的なのは用意されてなくて気合いのif文とt.Error関数でテストを書いていく。筋力が必要。

実行するとこんな感じになる。

=== RUN TestSave
--- PASS: TestSave (0.01 seconds)
=== RUN TestLoadPhotos
--- PASS: TestLoadPhotos (0.02 seconds)
=== RUN TestModels
Running Suite: Models Suite
===========================
Random Seed: 1402814295
Will run 5 of 5 specs

    •
Ran 5 of 5 Specs in 0.061 seconds
SUCCESS! -- 5 Passed | 0 Failed | 0 Pending | 0 Skipped --- PASS: TestModels (0.06 seconds)
PASS
ok      github.com/yuroyoro/go_shugyo/nethttp/models    0.143s

testing (GoConvey)

GoConveyというテスティングフレームワークを試してみる。BDDっぽい雰囲気。
`go get -t github.com/smartystreets/goconvey`で入れる。

package models

import (
	"fmt"
	. "github.com/smartystreets/goconvey/convey"
	"testing"
)

func TestConverySave(t *testing.T) {
	Convey("Insert", t, func() {
		DatabaseFile = "photos_test.db"
		dbmap, err := InitDb()

		if err != nil {
			t.FailNow()
		}

		defer dbmap.Db.Close()

		err = dbmap.TruncateTables()

		if err != nil {
			t.FailNow()
		}

		photo := Photo{
			URL:    "http://example.com",
			Author: "yuroyoro",
		}

		photo.Save()

		var photos []Photo

		_, err = dbmap.Select(&photos, "SELECT id, url, author FROM photos ORDER BY id ASC ")
		if err != nil {
			t.FailNow()
		}

		So(len(photos), ShouldEqual, 1)
		So(photos[0].URL, ShouldEqual, photo.URL)
		So(photos[0].Author, ShouldEqual, photo.Author)
	})
}

`So(len(photos), ShouldEqual, 1)`とかは、rspecとかに慣れてる人にはとっつきやすいのではないか。

GoConveyにはWebUIが付属しており、goconveyコマンドで起動するとlocalhost:8080でこんな画面が出てくる。ファイルの変更を検知して、自動的にテストを実行してくれる簡易CIのようだ。

f:id:yuroyoro:20140615172846p:plain

testing (Ginkgo)

今度は、GinkgoというBDDテスティングフレームワークを試す。assertionライブラリとしてgomegaというパッケージに切りだされている。

go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega


ginkgo bootstrapでtestのひな形が作成される。 テストコードはこんな感じになる。rspec風だが、func()の連打が辛み。

package models_test

import (
	"fmt"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	"github.com/coopernurse/gorp"
	"github.com/yuroyoro/go_shugyo/nethttp/models"
	"testing"
)

func TestModels(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Models Suite")
}

var _ = Describe("Photo", func() {
	var (
		dbmap *gorp.DbMap
		err   error
	)

	BeforeEach(func() {
		models.DatabaseFile = "photos_test.db"
		dbmap, err = models.InitDb()
		if err != nil {
			panic(err)
		}

		err = dbmap.TruncateTables()
		if err != nil {
			panic(err)
		}
	})

	AfterEach(func() {
		dbmap.Db.Close()
	})

	Describe("Insert", func() {
		It("should inserts new record", func() {
			photo := models.Photo{
				URL:    "http://example.com",
				Author: "yuroyoro",
			}

			photo.Save()

			var photos []models.Photo

			_, err = dbmap.Select(&photos, "SELECT id, url, author FROM photos ORDER BY id ASC ")
			if err != nil {
				Fail("Failed to load records from database")
			}

			Expect(len(photos)).To(Equal(1))
			Expect(photos[0].URL).To(Equal(photo.URL))
			Expect(photos[0].Author).To(Equal(photo.Author))

		})
	})
})

ginkgo -vでテスト実行。

ozaki@mbp-2 ( ꒪⌓꒪) $ ginkgo -v
............Running Suite: Models Suite
===========================
Random Seed: 1402820295
Will run 5 of 5 specs

Photo Insert
  should inserts new record
  /Users/ozaki/dev/go/src/github.com/yuroyoro/golang_webapp_framework_samples/nethttp/models/models_suite_test.go:61
•
------------------------------
Photo LoadPhotos when given page 0
  should returns first page
  /Users/ozaki/dev/go/src/github.com/yuroyoro/golang_webapp_framework_samples/nethttp/models/models_suite_test.go:87
•
------------------------------
Photo LoadPhotos when given page 2
  should returns last page
  /Users/ozaki/dev/go/src/github.com/yuroyoro/golang_webapp_framework_samples/nethttp/models/models_suite_test.go:101
•
------------------------------
Photo LoadPhotos when given page 99
  should returns empty
  /Users/ozaki/dev/go/src/github.com/yuroyoro/golang_webapp_framework_samples/nethttp/models/models_suite_test.go:110
•
------------------------------
Photo LoadPhotos when given page -1
  should returns error
  /Users/ozaki/dev/go/src/github.com/yuroyoro/golang_webapp_framework_samples/nethttp/models/models_suite_test.go:119
•
Ran 5 of 5 Specs in 0.054 seconds
SUCCESS! -- 5 Passed | 0 Failed | 0 Pending | 0 Skipped PASS

Ginkgo ran in 1.521415799s
Test Suite Passed


スティングについてはここまで。ほかにも色々あるので、「go言語のテスティングフレームワークについて — さにあらず」などを参考にサレタシ。

Martini編

さて、前置きが長くなったが、GolangでのWeb Application FrameworkであるMartiniを試してみる。
日本語のドキュメントもある。

net/httpで書いたサンプルを移植する。ソースコードこちら

package main

import (
	"./models"
	"fmt"
	"github.com/codegangsta/martini"
	"github.com/codegangsta/martini-contrib/render"
	"net/http"
	"strconv"
)

func main() {
	m := martini.Classic()

	m.Use(render.Renderer())
	m.Use(martini.Static("views"))

	m.Get("/", func(w http.ResponseWriter, r *http.Request, render render.Render) {
		page, err := strconv.Atoi(r.URL.Path[1:])
		if err != nil {
			page = 0
		}

		body, err := loadBody(page)
		if err != nil {
			panic(err)
		}

		render.HTML(200, "index", body)
	})

	m.Post("/", func(w http.ResponseWriter, r *http.Request, render render.Render) {
		url := r.FormValue("url")
		author := r.FormValue("author")

		if url == "" {
			render.Error(500)
			return
		}

		if author == "" {
			render.Error(500)
			return
		}

		fmt.Printf("Save Photo(%s, %s)", url, author)
		photo := models.Photo{
			URL:    url,
			Author: author,
		}
		photo.Save()

		render.Redirect("/", 302)
	})

	m.Run()
}

みてのとおり、Sinatra風だ。m.Getやm.Postに関数を登録している。net/httpと異なる点として、render.Renderを利用してステータスコードや、テンプレートのレンダリングを実行している。


登録する関数は実は任意の引数、戻り値にすることが可能で、Martiniはリフレクションを用いて引数や戻り値の解決をしている。上記のコードで引数で受けているrender.Renderは、Martiniの持つDI機能によって、リフレクションで動的に解決されている。

以下はサンプルより。戻り値としてステータスコードとbodyの中身を返している。

m.Get("/", func() (int, string) {
  return 418, "i'm a teapot" // HTTP 418 : "i'm a teapot"
})

他にも、引数に構造体を受け取る関数を登録すると、リクエストパラメータを構造体にmappingしてくれたりするらしい。このあたりのリフレクションを用いた黒魔術が「Go的ではない」という理由で批判されていたりもする。

Revel編

次は、Revelを試してみる。MartiniがSinatraだとすると、RevelはRailsに相当する感じ。コードはこちら


revelコマンドで色々と出来るようになっている。

ozaki@mbp-2 ( ꒪⌓꒪) $ revel
~
~ revel! http://revel.github.io
~
usage: revel command [arguments]

The commands are:

    new         create a skeleton Revel application
    run         run a Revel application
    build       build a Revel application (e.g. for deployment)
    package     package a Revel application (e.g. for deployment)
    clean       clean a Revel application's temp files
    test        run all tests from the command-line

Use "revel help [command]" for more information.

アプリケーション生成

revel new myappでアプリケーションのひな形ができる。Rails風。

ozaki@mbp-2 ( ꒪⌓꒪) $ revel new myapp
~
~ revel! http://revel.github.io
~
Your application is ready:
   /Users/ozaki/dev/go/src/myapp

You can run it with:
   revel run myapp


ディレクトリ構成はこうなっている。app以下にcontroller/viewsがある。

ozaki@mbp-2 ( ꒪⌓꒪) $ tree .
.
├── app
│   ├── controllers
│   │   └── app.go
│   ├── init.go
│   └── views
│       ├── App
│       │   └── Index.html
│       ├── debug.html
│       ├── errors
│       │   ├── 404.html
│       │   └── 500.html
│       ├── flash.html
│       ├── footer.html
│       └── header.html
├── conf
│   ├── app.conf
│   └── routes
├── messages
│   └── sample.en
├── public
│   ├── css
│   │   └── bootstrap.css
│   ├── img
│   │   ├── favicon.png
│   │   ├── glyphicons-halflings-white.png
│   │   └── glyphicons-halflings.png
│   └── js
│       └── jquery-1.9.1.min.js
└── tests
    └── apptest.go


revel run myappでサーバー起動。

ozaki@mbp-2 ( ꒪⌓꒪) $ revel run myapp
~
~ revel! http://revel.github.io
~
INFO  2014/06/15 16:34:46 revel.go:320: Loaded module static
INFO  2014/06/15 16:34:46 revel.go:320: Loaded module testrunner
INFO  2014/06/15 16:34:46 run.go:57: Running myapp (myapp) in dev mode
INFO  2014/06/15 16:34:46 harness.go:165: Listening on :9000


f:id:yuroyoro:20140615172906p:plain

昔のPlayっぽいアレだ。ヒィッ...。

ルーティング

さて、実装だが。まずはconf/routesにルーティングの定義を書く。

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

module:testrunner

GET     /                                       App.Index
GET     /photos/*                               Photos.Index
POST    /photos                                 Photos.Save

# Ignore favicon requests
GET     /favicon.ico                            404

# Map static resources from the /app/public folder to the /public path
GET     /css/*filepath                       Static.Serve("public/css")

# Catch all
*       /:controller/:action                    :controller.:action

あれ、なんかPlayっぽい……。'/hotels/:id 'のようにpathからparameterを切り出す機能もある。routesの定義は型安全である必要がある。つまり、controllerに対応するメソッドがないとエラーになる。うーんPlay……。

controller

さて、controller側の実装だが。生成されたapp/controllers/app.goを見てみると、Appという構造体がある。これがアプリケーションで使うcontrollerのベースとなるもので、各controllerはこのAppを構造体埋め込みで埋め込んでおくことで、フレームワークが提供する様々な機能を利用することができるようだ。

package controllers

import "github.com/revel/revel"

type App struct {
	*revel.Controller
	GorpController
}

func (c App) Index() revel.Result {
	return c.Render()
}


今回のアプリケーションでは、Gorpを用いたDatabaseへのアクセスやトランザクションを提供するGorpControllerを用意してAppに埋め込むことで、Database関連の機能をcontrollerに追加している。

package controllers

import (
	"database/sql"
	"github.com/coopernurse/gorp"
	_ "github.com/mattn/go-sqlite3"
	r "github.com/revel/revel"
	"github.com/revel/revel/modules/db/app"
	m "github.com/yuroyoro/go_shugyo/revel_sample/app/models"
)

var (
	Dbm *gorp.DbMap
)

func InitDB() {
	db.Init()
	Dbm = &gorp.DbMap{Db: db.Db, Dialect: gorp.SqliteDialect{}}

	Dbm.AddTableWithName(m.Photo{}, "photos").SetKeys(true, "Id")

	Dbm.TraceOn("[gorp]", r.INFO)

	err := Dbm.CreateTablesIfNotExists()
	if err != nil {
		panic(err)
	}

	photos := []*m.Photo{
		&m.Photo{URL: "http://24.media.tumblr.com/d6b9403c704c3e5aa1725c106e8a9430/tumblr_mvyxd9PUpZ1st5lhmo1_1280.jpg", Author: "Dillon McIntosh"},
	}

	for _, photo := range photos {
		if err := Dbm.Insert(photo); err != nil {
			panic(err)
		}
	}

}

type GorpController struct {
	*r.Controller
	Txn *gorp.Transaction
}

func (c *GorpController) Begin() r.Result {
	txn, err := Dbm.Begin()
	if err != nil {
		panic(err)
	}
	c.Txn = txn
	return nil
}

func (c *GorpController) Commit() r.Result {
	if c.Txn == nil {
		return nil
	}
	if err := c.Txn.Commit(); err != nil && err != sql.ErrTxDone {
		panic(err)
	}
	c.Txn = nil
	return nil
}

func (c *GorpController) Rollback() r.Result {
	if c.Txn == nil {
		return nil
	}
	if err := c.Txn.Rollback(); err != nil && err != sql.ErrTxDone {
		panic(err)
	}
	c.Txn = nil
	return nil
}


具体的なcontrollerの実装は、こんな感じになった。Photo構造体にAppを埋め込んでいる。

package controllers

import (
	"fmt"
	"github.com/revel/revel"
	"github.com/yuroyoro/go_shugyo/revel_sample/app/models"
	"github.com/yuroyoro/go_shugyo/revel_sample/app/routes"
)

type Photos struct {
	App
}

func (c Photos) Index(page int) revel.Result {

	records, err := models.LoadPhotos(c.Txn, page)

	if err != nil {
		panic(err)
	}

	fmt.Println(records)

	first := records[0]
	photos := records[1:]

	return c.Render(first, photos)
}

func (c Photos) Save(photo models.Photo) revel.Result {

	photo.Validate(c.Validation)

	if c.Validation.HasErrors() {
		c.Validation.Keep()
		c.FlashParams()
		return c.Redirect(routes.Photos.Index(0))
	}

	err := c.Txn.Insert(&photo)
	if err != nil {
		panic(err)
	}

	return c.Redirect(routes.Photos.Index(0))
}


RevelのcontrollerもMartini同様、関数の引数へのData-Bindingの機能を提供している。レスポンスについては、c.Renderでapp/views/template以下から適切なファイルが選択されてレンダリングされるようになっている。テンプレート自体は、html/templateをRevelが拡張したもので、ほぼ同じように使うことができる。

まとめ

駆け足で、GolangでのWeb Application作成を3つのフレームワークを用いて紹介してみた。他にも、Martiniへのカウンターとしてのhttps://github.com/codegangsta/negroni:Negroniや、静的ファイルもバイナリにまとめてることができるKochaなどがある。

個人的には、Revelはちょっと重量級でMartiniくらいがちょうどよいが、今後もSinatraRailsみたいな位置づけでそれぞれ使い分ける感じになるのではなかろうか。

以下、参考URL。

ʕ  ゚皿゚ ʔ GolangからLevelDBを使う

cgoとLevelDBを使って、タイトルのとおりのものを作ってみた。頑張ればRiakとかInfluxdbみたいなのを書けるかもナー。
ʕ  ゚皿゚ ʔ cgo楽しいおシーゴォー


コードはすべてGithubにある。

yuroyoro/leveldb-go-sample · GitHub


なお、この実装はあくまで個人的な練習で作ったものなので、まともにLevelDBをGoから使うならばInfluxdbでも使ってるlevigoがおすすめ。
LevelDBはあらかじめinstallしてある想定。 mac osxなのでbrew install leveldbで入った。

cgoでLevelDBをwrapする

まずは、cgoを使ったLevelDBの簡単なwrapperを用意する。単にLevelDBを使うだけなら、感覚的にはsqlite3みたいに、leveldb_openでopenして得られるleveldb_t構造体を使ってputやgetを呼び出し、終わったらcloseすればいい。

leveldb.go から抜粋。

package main

// #cgo LDFLAGS: -lleveldb
// #include <stdlib.h>
// #include "leveldb/c.h"
import "C"

import (
	"errors"
	"unsafe"
)

// C Level pointer holder
type LevelDB struct {
	CLevelDB *C.leveldb_t
	Name     string
}

// Open LevelDB with given name
func OpenLevelDB(path string) (leveldb *LevelDB, err error) {

	cpath := C.CString(path) // convert path to c string
	defer C.leveldb_free(unsafe.Pointer(cpath))

	// allocate LevelDB Option struct to open
	opt := C.leveldb_options_create()
	defer C.leveldb_free(unsafe.Pointer(opt))

	// set open option
	C.leveldb_options_set_create_if_missing(opt, C.uchar(1))

	// open leveldb
	var cerr *C.char
	cleveldb := C.leveldb_open(opt, cpath, &cerr)

	if cerr != nil {
		defer C.leveldb_free(unsafe.Pointer(cerr))
		return nil, errors.New(C.GoString(cerr))
	}

	return &LevelDB{cleveldb, path}, nil
}


上記のOpenLevelDBで、leveldb_openでdatabaseを開く。optionは色々指定できるのだが、サンプルなのでcreate_if_missingだけ指定している。

// Put key, value to database
func (db *LevelDB) Put(key, value string) (err error) {

	opt := C.leveldb_writeoptions_create() // write option
	defer C.leveldb_free(unsafe.Pointer(opt))

	k := C.CString(key) // copy
	defer C.leveldb_free(unsafe.Pointer(k))

	v := C.CString(value)
	defer C.leveldb_free(unsafe.Pointer(v))

	var cerr *C.char
	C.leveldb_put(db.CLevelDB, opt, k, C.size_t(len(key)), v, C.size_t(len(value)), &cerr)

	if cerr != nil {
		defer C.leveldb_free(unsafe.Pointer(cerr))
		return errors.New(C.GoString(cerr))
	}

	return
}

func (db *LevelDB) Get(key string) (value string, err error) {

	opt := C.leveldb_readoptions_create() // write option
	defer C.leveldb_free(unsafe.Pointer(opt))

	k := C.CString(key) // copy
	defer C.leveldb_free(unsafe.Pointer(k))

	var vallen C.size_t
	var cerr *C.char
	cvalue := C.leveldb_get(db.CLevelDB, opt, k, C.size_t(len(key)), &vallen, &cerr)

	if cerr != nil {
		defer C.leveldb_free(unsafe.Pointer(cerr))
		return "", errors.New(C.GoString(cerr))
	}

	if cvalue == nil {
		return "", nil
	}

	defer C.leveldb_free(unsafe.Pointer(cvalue))
	return C.GoString(cvalue), nil
}


put/getはこんな感じ。C.CStringでkeyやvalueをCの*charに変換しているが、これはmemcpyが走る(ハズ?)なので効率的ではない。levigoの実装では、[]byteに変換してunsafe.Pointerでgoのbyte列のpointerをC側に渡す実装になっているようだ。

net/httpでRESTっぽいガワをつける

goを勉強した事がある人なら誰しもがnet/httpを使った簡単なkvsを書いたことがあるはず。今回はバックエンドをLevelDBにして、net/httpでREST的なガワをつけてみる。

まずはBackend側の実装。 backend.go から。

package main

import (
	"log"
)

type Backend struct {
	db     *LevelDB
	putch  chan putRequest
	delch  chan delRequest
	quitch chan bool
}

type putRequest struct {
	key string
	val string
}

type delRequest struct {
	key string
}

func NewBackend(dbname string) (backend *Backend, err error) {
	db, err := OpenLevelDB(dbname)

	if err != nil {
		return
	}

	log.Printf("LevelDB opened : name -> %s", dbname)

	backend = &Backend{
		db,
		make(chan putRequest),
		make(chan delRequest),
		make(chan bool),
	}

	return
}

func (backend *Backend) Start() {
	go func() {
		for {
			select {
			case putreq := <-backend.putch:
				backend.db.Put(putreq.key, putreq.val)
				log.Printf("Backend.Put(%s, %v)\n", putreq.key, putreq.val)
			case delreq := <-backend.delch:
				backend.db.Delete(delreq.key)
				log.Printf("Backend.Delete(%s)\n", delreq.key)
			case <-backend.quitch:
				close(backend.putch)
				close(backend.delch)
				close(backend.quitch)

				log.Printf("Backend stoped")
				return
			}
		}
	}()

	log.Printf("Backend started")
}

func (backend *Backend) Shutdown() {
	backend.quitch <- true
}

func (backend *Backend) Get(key string) (val string, err error) {
	return backend.db.Get(key)
}

func (backend *Backend) Put(key, val string) {
	backend.putch <- putRequest{key, val}

}

func (backend *Backend) Delete(key string) {
	backend.delch <- delRequest{key}
}


NewBackendで先ほど用意したwrapperを使ってLevelDBをopenして、goroutine経由でput/deleteするように実装している。本来ならば、goroutineをsyncして同期っぽいAPIにするべきなのだろうが、面倒なのでサボっている。


次に、net/httpを使ってhttpを処理するserver側。 server.go から。

package main

import (
	"flag"
	"io/ioutil"
	"log"
	"net/http"
)

func main() {

	// parse command line arguments
	dbname := flag.String("name", "./testdb", "Open the database with the specified name")
	addr := flag.String("addr", ":5050", "listen address")

	flag.Parse()

	// open database and start backend
	backend, err := NewBackend(*dbname)
	if err != nil {
		log.Fatal("can't open database", err)
	}

	backend.Start()
	defer backend.Shutdown()

	// listen and serve http
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

		switch r.Method {
		case "GET":
			HandleGet(w, r, backend)
		case "POST", "PUT":
			HandlePut(w, r, backend)
		case "DELETE":
			HandleDelete(w, r, backend)
		}
	})

	log.Printf("Server listening on : %s", *addr)
	http.ListenAndServe(*addr, nil)
}

func HandleGet(w http.ResponseWriter, r *http.Request, backend *Backend) {
	key := r.URL.Path[len("/"):]

	val, err := backend.Get(key)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	if val == "" {
		w.WriteHeader(http.StatusNotFound)
		return
	}

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(val))
}

func HandlePut(w http.ResponseWriter, r *http.Request, backend *Backend) {
	key := r.URL.Path[len("/"):]

	defer r.Body.Close()
	val, err := ioutil.ReadAll(r.Body)

	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	backend.Put(key, string(val))

	w.WriteHeader(http.StatusCreated)
}

func HandleDelete(w http.ResponseWriter, r *http.Request, backend *Backend) {
	key := r.URL.Path[len("/"):]

	backend.Delete(key)

	w.WriteHeader(http.StatusNoContent)
}

起動時の引数でdbのpathとportを貰ってlisten。httpでGET/POST/PUT/DELETEをそれぞれ処理している。見たとおりの実装で、特に難しいことはしていない。

Dockerfile

Dockerfileもあるので、手元でdockerが動けば動作させてみることができる。Dockerhubでimageを配布しようとしたのだが、なぜかpendingのままでimageをbuildしてくれない( ;゚皿゚)。

Dockerfile