【Go言語】(ベンチマークあり)構造体のスライスから1つのフィールドを抽出してみる

こんにちは!eurekaでサーバサイドエンジニアをやっている寿福です。

サーバサイド開発をしている時、特にDB操作周りで取得したデータを格納した構造体から特定のフィールドを抽出してIN句に投げたい時ってありますよね。

Go言語で開発している時にその処理をライブラリに抜き出せないかと思い試してみたのでご紹介します。

まずはシンプルにfor文で直接抜き出すパターンです。

type Hoge struct {
ID int64
Name string
}
type Hoge2 struct {
ID2 int64
Name2 string
}
var (
list = []Hoge{{1, "Mark"}, {2, "John"}, {3, "Bob"}, {4, "Alex"}}
list2 = []Hoge2{{1, "Mark"}, {2, "John"}, {3, "Bob"}, {4, "Alex"}}
)
// ①for文で直接抜き出すパターン
func Benchmark_NormalLoop(b *testing.B) {
for i := 0; i < b.N; i++ {
ids := make([]int64, len(list))
for i, v := range list {
ids[i] = v.ID
}
ids2 := make([]int64, len(list2))
for i, v := range list2 {
ids2[i] = v.ID2
}
}
}

上記の場合、構造体ごとにfor文を書かなければならずあまり美しくありませんね。


次にライブラリ化する為、reflectパッケージを使用し参照するフィールド名を外から渡すようにしたパターンです。

type Hoge struct {
ID int64
Name string
}
type Hoge2 struct {
ID2 int64
Name2 string
}
var (
list = []Hoge{{1, "Mark"}, {2, "John"}, {3, "Bob"}, {4, "Alex"}}
list2 = []Hoge2{{1, "Mark"}, {2, "John"}, {3, "Bob"}, {4, "Alex"}}
)
// ②reflectパッケージで処理するパターン
func Benchmark_ReflectLoop(b *testing.B) {
for i := 0; i < b.N; i++ {
GetInt64SlicesBySpecifiedField(&list, `ID`)
GetInt64SlicesBySpecifiedField(&list2, `ID2`)
}
}
func GetInt64SlicesBySpecifiedField(p interface{}, fieldName string) []int64 {
v := reflect.ValueOf(p).Elem()
len := v.Len()
slices := make([]int64, len)
switch v.Kind() {
case reflect.Slice:
for i := 0; i < len; i++ {
slices[i] = v.Index(i).FieldByName(fieldName).Int()
}
}
return slices
}

これでfor文の繰り返しはなくなりスッキリしました。が・・ベンチマークしてみると処理時間が約16倍になってしまいました。。

$ go test -bench . -benchmem
Benchmark_NormalLoop-4 30000000 46.6 ns/op
Benchmark_ReflectLoop-4 2000000 748 ns/op

なので最終的に構造体ごとにfor文を書くこととし、特定のフィールドを抽出する用にインターフェースを用意して、呼び出し元から共通関数で処理させるようにしてみました。

type Hoge struct {
ID int64
Name string
}
type Hoge2 struct {
ID2 int64
Name2 string
}
// ③IFでの実装パターン
func Benchmark_IFLoop(b *testing.B) {
for i := 0; i < b.N; i++ {
getIDs(list3)
getIDs(list4)
}
}
type HogesI interface {
GetIDs() []int64
}
func getIDs(list HogesI) []int64 {
return list.GetIDs()
}
type Hoges []Hoge
func (h *Hoges) GetIDs() []int64 {
ids := make([]int64, len(*h))
for i, v := range *h {
ids[i] = v.ID
}
return ids
}
type Hoges2 []Hoge2
func (h *Hoges2) GetIDs() []int64 {
ids := make([]int64, len(*h))
for i, v := range *h {
ids[i] = v.ID2
}
return ids
}
var (
list3 = &Hoges{
Hoge{1, "Mark"},
Hoge{2, "John"},
Hoge{3, "Bob"},
Hoge{4, "Alex"},
}
list4 = &Hoges2{
Hoge2{1, "Mark"},
Hoge2{2, "John"},
Hoge2{3, "Bob"},
Hoge2{4, "Alex"},
}
)

ベンチマークの結果も特に問題なさそうです。

$ go test -bench . -benchmem
Benchmark_NormalLoop-4 30000000 46.6 ns/op
Benchmark_ReflectLoop-4 2000000 748 ns/op
Benchmark_IFLoop-4 20000000 55.8 ns/op

最後に全パターンのソースコードもこちらに貼っておきます。

https://gist.github.com/jufuku/14ec0d6a7c27d910d435a35f68c6fa5e

わざわざインタフェースを用意しなければならないのがいまいちですが、もし他にこんな方法があるよと言う方がいればコメントいただけると嬉しいです。

また弊社では一ヶ月に一回Goもくもく会を開催しております。基本的に各自の作業するだけでなく、LT等簡単なイベントも少々考えているので、もしよろしければご参加くだいさい。