読者です 読者をやめる 読者になる 読者になる

( ꒪⌓꒪) ゆるよろ日記

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

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

Golang c++

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