【Golang】Goji + mgo で簡易 API Server をつくる

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の生産性の高さと性能の両立を目指すアプローチにはやはり魅力を感じます。