Gojiとmgoを使って1時間くらいで超簡易API Serverを作ったのでメモ。
動機はGolangを最近書いてなかったので書きたくなった…くらいしかないです。
環境は Ubuntu 14.04で Golang 1.4を入れました。
$ wget https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz
$ sudo tar zxvf ~/go1.4.linux-amd64.tar.gz -C /usr/local
$ vim ~/.bashrc
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
$ go version
go version go1.4 linux/amd64
Goji
Gojiは A web microframework で, 下記のような特徴があります。
- net/http との互換性
 - SinatraスタイルなURLパターンと正規表現パターンに加えて, カスタムパターンを定義可能
 - ミドルウェア機構
 - Einhorn, systemdをサポート
 - グレースフルシャットダウン (Einhornを使うと zero-downtime graceful reload)
 - WAFの中でも高速な部類 (Go HTTP request router and web framework benchmark)
 
Gojiのインストールです。
$ go get github.com/zenazn/goji
公式に書かれている Hello に URLパラメータを繋げて返す例。
default portは 8000 ですが, flag.Set(“bind”, “8080”) のようにflagをsetして変更できます。
package main
import (
        "fmt"
        "net/http"
        "github.com/zenazn/goji"
        "github.com/zenazn/goji/web"
)
func hello(c web.C, w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %s!", c.URLParams["name"])
}
func main() {
        goji.Get("/hello/:name", hello)
        goji.Serve()
}
mgo
mgoは Golang向けの MongoDB driver で, 下記のような特徴があります。
- Clusterの発見/通信機能
 - master serverの変化を検知して, 自動的に Failover
 - 柔軟なシリアライゼーション (marshall / unmarshall)
 - 同期インターフェイスと並行性
 - GridFSのサポート
 
mgoのインストールです。
$ go get gopkg.in/mgo.v2
公式に書かれている MongoDBに接続し insertと findOneを実行する例です。
package main
import (
        "fmt"
        "log"
        "gopkg.in/mgo.v2"
        "gopkg.in/mgo.v2/bson"
)
type Person struct {
        Name string
        Phone string
}
func main() {
        session, err := mgo.Dial("server1.example.com,server2.example.com")
        if err != nil {
                panic(err)
        }
        defer session.Close()
        // Optional. Switch the session to a monotonic behavior.
        session.SetMode(mgo.Monotonic, true)
        c := session.DB("test").C("people")
        err = c.Insert(&Person{"Ale", "+55 53 8116 9639"},
	               &Person{"Cla", "+55 53 8402 8510"})
        if err != nil {
                log.Fatal(err)
        }
        result := Person{}
        err = c.Find(bson.M{"name": "Ale"}).One(&result)
        if err != nil {
                log.Fatal(err)
        }
        fmt.Println("Phone:", result.Phone)
}
おわりに
全体のコードとしては下記で, 定期的に Web scrapingしている複数のマシンで成否や失敗理由などの結果を jsonで返すAPI Serverです。
色々足りてないが趣味コードなので, これでも事足りてしまっている…
package main
import (
	"encoding/json"
	"flag"
	"log"
	"net/http"
	"github.com/zenazn/goji"
	"github.com/zenazn/goji/web"
	"gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)
type ErrorRes struct {
	Status  int    `json:"status-code"`
	Message string `json:"message"`
}
type Log struct {
	ID          bson.ObjectId `bson:"_id"`
	Account     string        `bson:"account"`
	IsSucceeded bool          `bson:"isSucceeded"`
	Site        string        `bson:"site"`
	Date        string        `bson:"date"`
	Message     []string      `bson:"message"`
}
func findAll() (r []Log) {
	session, err := mgo.Dial("mongodb://localhost")
	if err != nil {
		panic(err)
	}
	defer session.Close()
	session.SetMode(mgo.Monotonic, true)
	con := session.DB("db-name").C("log")
	err = con.Find(bson.M{}).All(&r)
	if err != nil {
		log.Fatal(err)
	}
	return r
}
func getLog(c web.C, w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	encoder := json.NewEncoder(w)
	if c.URLParams["APPID"] != "[Your APP ID]" {
		encoder.Encode(&ErrorRes{401, "Incorrect App ID"})
		return
	}
	result := findAll()
	encoder.Encode(result)
}
func main() {
	flag.Set("bind", ":8888")
	goji.Get("/log/:APPID", getLog)
	goji.Get("/logs/:APPID", http.RedirectHandler("/log/:APPID", 301))
	goji.Serve()
}
Golangの生産性の高さと性能の両立を目指すアプローチにはやはり魅力を感じます。