近所の @bobpp 氏が書いて社内で運用している pastel というコピー&ペーストをシェアするウェブアプリ(gist の簡易版だと思いねぇ)を golang の勉強の一環で golang に fork してみた。

https://github.com/sonots/go-pastel

前回 のセルフ golang 勉強会ではコマンドラインツールを作ったので、今度はウェブアプリを作ってみようと思ってその題材としてよさそうだなと思ってやってみた。

勉強メモ

ウェブサーバ

golang には net/http という標準ライブラリがあって、これでウェブサーバを書けるようになっている。さらに使いやすくする事を考えたフレームワークもいろいろあるようだけど、最初なので素の net/http でやってみることにした。yuroyoro さんの

の記事を参考にさせてもらった mm

template

golang にはこれまた標準で html/template という標準テンプレートライブラリがある。

<p>{{.Body}}</p>

みたいなかんじで {{}} に式を書けるようだ。

で、最初 {{}} の中の文法は erb で <% %> 内に ruby が直接書けるように直接 golang を書けるんだと思っていたけど、なんだかちょっと違う文法のようだった。

たとえば range なんかは golang だと

for index, photo := range obj.Photos {
}

みたいになるんだけど、template だと

{{range $index, $photo := .Photos}}
{{end}

みたいになって、微妙に違う。ちなみにテンプレートを展開するときに渡した object のメンバを .Photosみたいに . 始まりで呼び出すらしい。. だとそのオブジェクトそのもの。

こちらも yuroyoro さんの

の記事に書いてある。

template inheritance

template の継承というか include がやりたかったので調べた。次のようにやるようだ。

cf. https://github.com/golang-samples/template/tree/master/extends

base.html

{{define "base"}}
<html>
{{template content .}}
</html>
{{end}}

hoge.html

{{define "content"}}
{{.}}
{{end}}

main.go

package main

import (
    "html/template"
    "net/http"
)

var hogeTmpl = template.Must(template.New("hoge").ParseFiles("base.html", "hoge.html"))

func hogeHandler(w http.ResponseWriter, r *http.Request) {
    hogeTmpl.ExecuteTemplate(w, "base", "Hoge")
}

func main() {
    http.HandleFunc("/", hogeHandler)
}

まずポイントになるのは ParseFiles("base.html", "hoge.html") に二つファイルを渡している所。これで base.html を parse した後、続けて hoge.html を parse して結合してもらえる。

次のポイントは hogeTmpl.ExecuteTemplate(w, "base", "Hoge") で、第二引数の base で {{define "base"}}のテンプレートを展開している。第三引数の "Hoge" はテンプレートに渡しているオブジェクトである。

で、base.html が展開されると {{template content .}} で hoge.html の {{define "content"}} が展開される。

なお、{{template content .}} の . が ExecuteTemplate に渡されたオブジェクト("Hoge") で、それを content テンプレートにそのまま渡している。content テンプレートである hoge.html の中ではまた . でアクセスできるので {{.}} で表示されることになる。

erb とは違ってファイル名に引きずられないように設計されているように感じた。

database

database への接続には database/sql という統一インターフェースがあって、ドライバはそれぞれ勇士が作っているようだった。 ドライバの一覧はこちら にある。

今回は sqlite3 を使ったので ithub.com/mattn/go-sqlite3 を使わせていただいた。

gorp という ORM もあるらしいかったけど、最初だったのと、perl の DBI を使って直接クエリをながしているアプリの golang fork ということもあって、今回は ORM は使わないことにしてみた。

使い方は _example/simple.go を見ると良くて、 こんなかんじになった(error 処理省いて書いてる)。

var db, _ := sql.Open("sqlite3", filename)

var id int64
err := db.QueryRow("SELECT id FROM memos WHERE access_key = ?", key).Scan(&id)
switch {
case err == sql.ErrNoRows:
    http.NotFound(w, r)
    return
}

_, _ = db.Exec("DELETE FROM memos WHERE access_key = ?", key)

go-bindata

golang の良い所に、バイナリ1つを配置するとアプリケーションが動かせる、というところがあると思う。 ウェブアプリの場合 template ファイルなど他のリソースがあってやりづらいんだけど、 go-bindata というのがあって、 リソースファイルをバイナリにして go ファイルにまとめて、それを一緒にビルドしてバイナリにデータを組み込むという手法が使えるらしい。

使い方は

go get github.com/jteeuwen/go-bindata/...
go-bindata views/... static/...

とすると views, static 下のをまるっと bindata.go という golang コードに変換してくれるので、 go build 時に bindata.go も一緒に指定してビルドしてあげればよい。

bindata.go の中を読むと

func views_base_html() ([]byte, error) {
        return bindata_read([]byte{
                0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0xac, 0x91,
                0x3d, 0x6f, 0xf3, 0x30, 0x0c, 0x84, 0x77, 0xff, 0x0a, 0x86, 0x73, 0x62,
                0x21, 0x99, 0x5e, 0xe0, 0x95, 0xbd, 0xf4, 0x63, 0x6d, 0xd0, 0xa6, 0x43,
                0x47, 0xc6, 0xa2, 0x2b, 0xa1, 0xb2, 0x9c, 0x5a, 0x4c, 0xd0, 0xc0, 0xc8,
                0x7f, 0xaf, 0xfc, 0x11, 0x04, 0xc8, 0xdc, 0xc9, 0x16, 0x79, 0xf7, 0x80,
                0xb8, 0xeb, 0x7b, 0xc3, 0xb5, 0x0b, 0x0c, 0xb8, 0xa7, 0xc8, 0x78, 0xb9,

こんなかんじでバイナリが埋め込まれていた。

閑話休題: ちなみに ... は golang でよく使われるディレクトリ以下を再起的に探索するパスの指定法である。zsh にいつも間違ってない?と言われるのであとでなんとかしたい

で、bindata.go 内のデータを取り込むには、bindata.go に Asset 関数が定義されているのでそれを利用すればいいようだ。 たとえば

var formTempl = template.Must(template.New("form").ParseFiles("views/base.html", "views/form.html"))

と書いていたコードは

var baseHtml, _ = Asset("views/base.html")
var formHtml, _ = Asset("views/form.html")
var formTmpl = template.Must(template.New("form").Parse(string(baseHtml) + string(formHtml))

のように置き換えることができた。ただ、静的ファイルのサーブをする所は

http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

のように、ファイルシステムのディレクトリを直接参照させてしまっていたので、どうしたらいいんだろう?と思っていた。 bindata.go の中のデータを取り出してファイルシステム上にファイルを作る??

とか悩んでいたんだけど go-bindata-assetfs という便利なものがすでにあってそれを使うと簡単に置き換えができた。最高。File 構造体に対する mock のようなクラスを作って対応しているようだ。

http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(&assetfs.AssetFS{Asset, AssetDir, "/static/"})))

並列リクエスト数について

net/http において同時最大並列リクエスト数はどれぐらいなんだろう、と思ってちょっと調べてた。

net/http のリクエスト受付はイベントドリブン non-blocking で行われていて、簡単にいうと以下のような動きをしているらしい。

ソケットListen => Accept => 接続毎にGoルーチン起動 => Goルーチンの中で Handler を実行

なので、同時並列リクエスト数の限界は、実質 goroutine の最大数になるのだと思うけど、その値を設定する項目は軽く探した限りでは見つからなかった(教えてください)。

ちなみに goroutine も1000個ぐらいになると動作が遅くなってくるという情報があって、実際 ab -c 1000 -n 100000 http://localhost:5050 とかやってベンチマークをとって見ると、100 concurrency で 7000 requests / sec だったのが、1000 concurrency で 5000 requests / sec に落ち込んでしまった(なお、計測環境は貧弱な linux 仮想マシン)

concurrency1101001000
requests / sec2260.776385.197451.875439.44

やはり同時並列リクエスト数には制限をかけたいところである。

database の connection pool について

あと、goroutine でそれぞれのリクエストが処理されるわけなので、それぞれの goroutine で db connection をシェアしてしまったら通信が混ざったりするんじゃないか?どうなんだろう、と思ってちょっとしらべていた。

cf. http://stackoverflow.com/questions/17376207/how-to-share-mysql-connection-between-http-goroutines

The database/sql package manages the connection pooling automatically for you.

とのことでなんかいいかんじに connection pool してくれているらしい。golang の標準ライブラリ賢い。

ちなみに db のほうの最大 connection 数は db.SetMaxIdleConns で制御できるみたい。

コア数を増やす

golang には CRuby みたいに GVL はないし、goroutine を使って並列処理させれば CPU コアを有効活用してくれるはずなんだけど、なんだかコア1つしか使ってくれていなかった。

が、これについては GOMAXPROCS を設定すればよいだけ。これで linux のネイティブスレッドをいくつ使うのか制御できる。goroutine はそのスレッド内部で go runtime レベルでの concurrency を実現することになる。

cpus := runtime.NumCPU()
runtime.GOMAXPROCS(cpus)

デフォルトでコア数にしておいてくれて構わない感ある

go-sqlite3 のコンパイルエラー

yak shaving が過ぎて追うのを途中で辞めたんだけど、go-sqlite3 のクロスコンパイルで色々エラーが出て戦ってた。

cf. https://github.com/mattn/go-sqlite3/issues/106#issuecomment-49501859

~/go/src/github.com/mattn/go-sqlite3/sqlite3.c:92 unknown #: if
~/go/src/github.com/mattn/go-sqlite3/sqlite3.c:94 5c: No such file or directory: mingw.h

こんなのが出てこまってた。#if がわかりませんって意味不明だった。brew install go して go 1.3 にあがったら直ってしまった。

そしたら次に gox でこのエラーが出るようになってしまった(これは gox の話)

cf. https://github.com/mitchellh/gox/issues/17

これは export GOROOT=/usr/local/opt/go/libexec のようにして GOROOT の場所を変えたら直ってしまった。

で、未だに出続けるのがこちら

cf. https://github.com/mattn/go-sqlite3/issues/32

# github.com/mattn/go-sqlite3
Undefined symbols for architecture x86_64:
  "_sqlite3_backup_finish", referenced from:
      __cgo_87e25ef90374_Cfunc_sqlite3_backup_finish in backup.cgo2.o
     (maybe you meant: __cgo_87e25ef90374_Cfunc_sqlite3_backup_finish)
  "_sqlite3_backup_init", referenced from:
      __cgo_87e25ef90374_Cfunc_sqlite3_backup_init in backup.cgo2.o
     (maybe you meant: __cgo_87e25ef90374_Cfunc_sqlite3_backup_init)
  "_sqlite3_backup_pagecount", referenced from:

全文: https://gist.github.com/sonots/4a73d0d8808f19f71408

Mac OSX 上でのビルドが失敗する。linux, windows の cross compile は成功する。

yak shaving ....