RubyのライブラリをGoに移植したくなった話


TL;DR


  • Rubyのライブラリである、RedBlocks をGoに移植している話

  • Rubyの継承などはEmbeddingで解決した

  • 仕方なくReflectionを使ってしまったが案外速度は問題なさそう


何を移植したくなったか

RubyにはRedisのset/zsetの操作(SINTERSTORE, ZINTERSTORE)とデータの取得を宣言的に扱うためのライブラリであるRedBlocks というのが存在します。

Readmeからリンクされている[ブログ](を見てみると

class RegionSet < RedBlocks::Set

def initialize(region)
raise ArgumentError unless Project::REGIONS.include?(region)
@region = region
end

def key_suffix
@region
end

def get
Project.where(region_cd: @region).pluck(:id)
end
end

region1_set = RegionSet.new('hokkaido')

region2_set = RegionSet.new('kanto')
regions_set = RedBlocks::UnionSet.new([region1_set, region2_set])
regions_set.ids #=> [921, 324, 21, 39, 101]

http://takenos.link/post/173086712782/redis-sorted-set-in-oop

このような形で、北海道もしくは関東の募集(=Project)が取得できます。

実際Redisを直に触ってしまうとこのような計算はもっと複雑になってしまいます。

これを使い、Goでも同じように楽にRedisの操作がしたくなりました。


何が問題か

問題点は基本的に2つありました。

1. Rubyの継承をどうGoで再現するか

2. クラス名(GoではStruct名)をどう取得するか

基本的にRedBlocksは継承を前提としており、RedBlocks::Setの場合、3つのメソッドを定義するだけでidsといったメソッドが自動で生えてきます。

これをGoでどうやるべきでしょうか。

また、RedBlocksでは半自動でキー名を作成してきます。

基本的にキーはクラス名とユーザーが設定するキーからなります。RedBlocks::Set#key

そのうち、クラス名に関してはGoではどう取るべきかが悩ましいです。


できたもの

RedBlocksをGoで実装したものがredblocks-go になります。

先程のRegionを作り、北海道または関東のものを取ってくる例がこちらになります。

func NewRegionSet(region string) set.Set {

return regionSetImp{region}
}

type regionSetImp struct {
region string
}

func (r regionSetImp) KeySuffix() string {
return r.region
}

func (r regionSetImp) Get(ctx context.Context) ([]set.IDWithScore, error) {
...
}

func (r regionSetImp) CacheTime() time.Duration {
return time.Second * 10
}

store := store.NewRedisStore(newPool())
tokyo := compose.Compose(NewRegionSet("tokyo"), store)
osaka := compose.Compose(NewRegionSet("kanto"), store)

set := operator.NewUnionSet(store, time.Second*100, tokyo, kanto)
set.IDs(ctx, options.WithPagenation(0, -1))


どう移植したか


Rubyの継承をどうGoで再現するか

GoにはEmbeddingというのがあります。Goのドキュメントにもあります https://golang.org/doc/effective_go.html#embedding

redblocks-goの例を出すと、次のようになります

type Set interface {

KeySuffix() string
Get(ctx context.Context) ([]IDWithScore, error)
CacheTime() time.Duration
NotAvailableTTL() time.Duration // NotAvailableTTL < CacheTime. For processing
}

https://github.com/rerost/redblocks-go/blob/master/pkg/set/set.go

type ComposedSet interface {

set.Set
Key() string
Update(ctx context.Context) error
Available(ctx context.Context) (bool, error)
Warmup(ctx context.Context) error
IDs(ctx context.Context, opts ...options.PagenationOption) ([]store.ID, error)
IDsWithScore(ctx context.Context, opts ...options.PagenationOption) ([]store.IDWithScore, error)
}

https://github.com/rerost/redblocks-go/blob/master/pkg/compose/set.go

このように、ユーザーが定義すべきものをSetというinterfaceにし、それをembedしたComposedSetというinterfaceを定義しています。

で、SetからComposedSetの必要なメソッドを定義する関数をComposeとしています。


クラス名(GoではStruct名)をどう取得するか

正直速度の観点から、Reflection をできるだけ使いたくないと思っていました。

しかし、実際クラス名の取得だけだとベンチマークを走らせてみると10%程度の違いしかなさそうなので問題ないと思っています。

~/.go/src/github.com/rerost/redblocks-go/pkg/compose (rerost/not-use-reflection)

$ go test -bench BenchmarkKey
goos: darwin
goarch: amd64
pkg: github.com/rerost/redblocks-go/pkg/compose
BenchmarkKey-4 5000000 277 ns/op
PASS
ok github.com/rerost/redblocks-go/pkg/compose 1.870s

https://github.com/rerost/redblocks-go/blob/27bd7d9590e6de38653ebe9235b3584a259fec83/pkg/compose/update.go#L28

~/.go/src/github.com/rerost/redblocks-go/pkg/compose (master)

user $ go test -bench BenchmarkKey
goos: darwin
goarch: amd64
pkg: github.com/rerost/redblocks-go/pkg/compose
BenchmarkKey-4 5000000 309 ns/op
PASS
ok github.com/rerost/redblocks-go/pkg/compose 2.060s

https://github.com/rerost/redblocks-go/blob/8a56b44f78b815bcf9cc2b9a4dad26e8ef457e2f/pkg/compose/update.go#L29


終わりに

RedBlocksのGo実装はまだ部分的に機能が足りないのですが、まあまあ使えるものにはなったかと思います。

以上です。