GoのMemcacheパッケージ比較

# はじめに
Go言語で作るプログラムはほとんどの場合、そのままでも十分高速に稼働しますが、場合によってはキャッシュを使用して高速化を図る場合があります。

Goには`gocache`という強力なローカルキャッシュパッケージが存在しますが、`gocache`だと下記の点でまれに問題が発生することがあります。
* ホスト・プログラム・サービスなどをまたいでキャッシュを参照させられない
* プログラムが終了するとキャッシュが消える

これらを回避するため、ローカルで済むキャッシュについては`gocache`でキャッシュしておき、それ以外のキャッシュは`memcache`や`redis`などに載せる、といった戦略を採ることが多いと思いますので、`memcache`パッケージについて比較してみます。
# Goのmemcacheパッケージ
Goの`memcache`パッケージは何があるのか、まずgithubで登録されているリポジトリを検索したところ、[bradfitz/gomemcache](https://github.com/bradfitz/gomemcache)が一番スターを獲得していたので、こちらを試しに使ってみましょう。
（以下すべて`go 1.5.1`です。）

パッケージの取り込み
```
$ go get github.com/bradfitz/gomemcache/memcache
```

実行サンプル

```go

import (
        "github.com/bradfitz/gomemcache/memcache"
)

func main() {
     mc := memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212")
     mc.Set(&memcache.Item{Key: "foo", Value: []byte("my value")})

     it, err := mc.Get("foo")
     ...
}
```

シンプルでわかりやすいですね。

せっかくなので、このパッケージのパフォーマンスがどれくらいなのかベンチマークを書いて計測します。
こういったベンチマークがすぐできるのがGoの強みでもあります。

```go:mem_test.go
package main

import(
	"testing"
	"github.com/bradfitz/gomemcache/memcache"
)

func BenchmarkMemLib(b *testing.B){
	b.ReportAllocs()
	b.ResetTimer()
	mc := memcache.New("127.0.0.1:11211")
	for i:= 0; i < b.N; i++{
		mc.Set(&memcache.Item{Key:"foo", Value:[]byte("unko")})
		mc.Get("foo")
	}
}

```

ベンチマークを実行します。

```
$ go test mem_test.go -bench=Bench
testing: warning: no tests to run
PASS
BenchmarkMemLib    20000             62516 ns/op            3169 B/op         60 allocs/op
ok      command-line-arguments  1.881s
```

単純なSetとGetのベンチにもかかわらず、アロケートが多いような・・・

他のパッケージと比較してみます。

次にスターが多かったのが[rainycape/memcache](https://github.com/rainycape/memcache)です。
READMEにも[bradfitz/gomemcache](https://github.com/bradfitz/gomemcache)を更に高速化するためにforkした、と書かれているので期待できそうです。

パッケージを取り込みます。

```
$ go get github.com/rainycape/memcache
```
実行サンプル

```go
import (
        "github.com/rainycape/memcache"
)

func main() {
     mc, _ := memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212")
     mc.Set(&memcache.Item{Key: "foo", Value: []byte("my value")})

     it, err := mc.Get("foo")
     ...
}
```
[bradfitz/gomemcache](https://github.com/bradfitz/gomemcache)とほとんど一緒ですが、こちらはNew()の戻り値が多値になっています（memcacheサーバはどちらも複数指定できます）。

こちらも先ほどのベンチマークに追加して計測してみましょう。

`BenchmarkMemLib2`が追加ベンチマークです。

```go:mem_test.go
package main

import(
	"testing"
	"github.com/bradfitz/gomemcache/memcache"
	memcache2 "github.com/rainycape/memcache"
)

func BenchmarkMemLib(b *testing.B){
	b.ReportAllocs()
	b.ResetTimer()
	mc := memcache.New("127.0.0.1:11211")
	for i:= 0; i < b.N; i++{
		mc.Set(&memcache.Item{Key:"foo", Value:[]byte("unko")})
		mc.Get("foo")
	}
}

func BenchmarkMemLib2(b *testing.B){
	b.ReportAllocs()
	b.ResetTimer()
	mc,_ := memcache2.New("127.0.0.1:11211")
	for i:= 0; i < b.N; i++{
		mc.Set(&memcache2.Item{Key:"foo", Value:[]byte("unko")})
		mc.Get("foo")
	}
}
```

実行結果

```
$ go test mem_test.go -bench=Bench
testing: warning: no tests to run
PASS
BenchmarkMemLib            20000             63543 ns/op            3169 B/op         60 allocs/op
BenchmarkMemLib2           20000             64594 ns/op             208 B/op          7 allocs/op
ok      command-line-arguments  3.854s
```

うーん、アロケーションは随分小さくなりましたが、処理速度は若干遅くなっています。
`memcache`サーバへのアクセス以外の処理で負荷がかかってる可能性がありますね。。。

# 自作する
ということで、自分でも書いてみることにしました（Memcacheまわりの処理は[vites](https://github.com/youtube/vitess/blob/master/go/memcache/memcache.go)のソースコードが大変参考になります）。

```go:mem.go
package main
import (
	"bufio"
	"fmt"
	"net"
	"strings"
	"strconv"
)

type Memcache struct {
	conn     net.Conn
	buffered bufio.ReadWriter
}

func Mem(addr string) (conn *Memcache, err error) {
	nc, err := net.Dial("tcp", addr)
	if err != nil {
		return nil, err
	}
	return &Memcache{
		conn: nc,
		buffered: bufio.ReadWriter{
			Reader: bufio.NewReader(nc),
			Writer: bufio.NewWriter(nc),
		},
	}, err
}

func (mc *Memcache) get(key string) (result []byte, err error) {
	_, err = mc.buffered.WriteString("get "+key+"\n")
	if err == nil {
		err = mc.buffered.Flush()
		if err == nil {
			for {
				b,_,err :=  mc.buffered.ReadLine()
				l := string(b)
				if err == nil {
					if strings.HasPrefix(l, "END") {
						break
					}
					if strings.Contains(l, "ERROR") {
						panic("ERROR")
					}
					if !strings.HasPrefix(l, "VALUE") {
						result = append(result, l...)
						result = append(result, '\n')
					}
				} else {
					panic(err)
				}
			}
		} else {
			panic(err)
		}
	}
	return result, err
}

func (mc *Memcache) set(key string, value []byte) (err error) {
	_, err = mc.buffered.WriteString("set "+key+" 0 0 "+strconv.Itoa(len(value))+"\r\n")
	if err == nil {
		v := append(value,"\r\n"...)
		_, err = mc.buffered.Write(v)
		if err != nil {
			panic(err)
		}
		err = mc.buffered.Flush()
		if err == nil {
			mc.buffered.ReadLine()
			
		}
	}
	return err
}

func main() {
	m, err := Mem("127.0.0.1:11211")
	if err == nil {
		err = m.set("foo",[]byte("unko"))
		if err == nil {
			res,merr := m.get("foo")
			if merr == nil {
				fmt.Printf("%s", res)
			}
		}
	}
	defer m.conn.Close()
}
```

ベンチマークを取ります。
`BenchmarkMemOrigin`が自分で書いたMemcache接続プログラムの結果です。

```go:mem_test.go
package main

import(
	"testing"
	"github.com/bradfitz/gomemcache/memcache"
	memcache2 "github.com/rainycape/memcache"
)

func BenchmarkMemLib(b *testing.B){
	b.ReportAllocs()
	b.ResetTimer()
	mc := memcache.New("127.0.0.1:11211")
	for i:= 0; i < b.N; i++{
		mc.Set(&memcache.Item{Key:"foo", Value:[]byte("unko")})
		mc.Get("foo")
	}
}

func BenchmarkMemLib2(b *testing.B){
	b.ReportAllocs()
	b.ResetTimer()
	mc,_ := memcache2.New("127.0.0.1:11211")
	for i:= 0; i < b.N; i++{
		mc.Set(&memcache2.Item{Key:"foo", Value:[]byte("unko")})
		mc.Get("foo")
	}
}

func BenchmarkMemOrigin(b *testing.B){
        b.ReportAllocs()
        b.ResetTimer()
        m, _ := Mem("127.0.0.1:11211")
        defer m.conn.Close()
        for i:= 0; i < b.N; i++{
                m.set("foo",[]byte("unko"))
                m.get("foo")
        }
}
```
実行結果

```
$ go test mem.go mem_test.go -bench=Bench
testing: warning: no tests to run
PASS
BenchmarkMemLib            20000             62850 ns/op            3169 B/op         60 allocs/op
BenchmarkMemLib2           20000             64663 ns/op             208 B/op          7 allocs/op
BenchmarkMemOrigin         50000             33910 ns/op              96 B/op          6 allocs/op
ok      command-line-arguments  5.879s
```

約半分くらいの処理速度になりました。

パッケージはエラー処理やcastなどが入っており安全に使用できることが考慮されているので必ずしも自作するべきだとは思いませんが、memcacheのSetやGetくらいの処理であれば、自作してしまったほうがパフォーマンス的に良さそうですね。

※パッケージごとのボトルネックは時間が無くて追えなかったので、後日調査します(;´Д｀)