LoginSignup
49
26

More than 5 years have passed since last update.

GolangのSliceでFilterやMap的なことをやりたい

Posted at

前までGo言語でFilter的なことをやりたい時はgo-funkを使っていたが、genを使うようになってからコードが読みやすくなったように感じるのでオススメしたい。

方法1: go-funkを使う

go-funkというライブラリを使うと、自前のstructのsliceに対してもFilterやMapなどの処理をかける。

result := funk.Filter(books, func(book *Book) bool {
    return book.Author == "John Smith"
})

Filter以外にもFindMapUniqなど非常に便利な関数が用意されている。手軽さを重視するなら、go-funkを使うのも悪くないとは思う。

ただし、go-funkはリフレクションの仕組みを使って実装されているため、返り値がinterface{}型のsliceになっているため毎回キャストが必要になったり、ビルド時の型チェックが効かないといったデメリットがある。

books := []*Book{
    {Author: "John Smith", Title: "My life"},
    {Author: "John Smith", Title: "Money"},
    {Author: "Paul Brown", Title: "Your life"},
}

titles := funk.Map(funk.Filter(books, func(b *Book) bool {
    return b.Author == "John Smith"
}), func(b *Book) string {
    return b.Title
}) // []interface{}型になってしまう

ビルド時の型チェックが通らないことが問題で、例えばこんなコードを書いたとしてもビルドできてしまう。

// booksは*Bookのスライスなのに、判定用の関数の引数は*Songになっている
titles := funk.Filter(books, func(s *Song) bool {
    return song.Name == "Your song"
}) 

手軽で便利なのだが、使っているとこのやり方はGo的ではないように感じる。

また、FilterもMapも関数の引数に配列を渡す形になるため、入れ子構造になって読みにくい。できれば下記のように書きたい。

titles := books.Filter(...).Map(...)

方法2: genを使う

go-funkは気軽に使えるという点では良いのだが、もっと無理なくGoらしくFilterを実現する方法がないか調べていたところ、genに辿り着いた。genはコードジェネレータなので、好みが分かれるかもしれないが、生成されるコードはビルド時の型チェックが有効になるし読みやすいので気に入っている。

genのコード生成を使ってFilter(Where)などのメソッドを生成すると下記のようなコードでFilterやMapを書けるようになる。

インストール

go get github.com/clipperhouse/gen

アノテーションを書く

// +gen * slice:"Where"
type Book struct {
    Author string
    Title  string
}

コード生成したい型の上に+genから始まるアノテーションを書く。今回はスライス用のコード生成の機能を使っているが、genには他のコード生成の機能も用意されている(があまり積極的に開発・利用されてなさそう)。

slice:Whereは、スライス用のジェネレータを使って、Whereメソッド用のコードを含める、という指示になっている。Where以外にも生成可能なメソッドは色々あるが、今回は使わないので省略した。gen: slice typewriter

+genslice:の間に*を書いているのは、スライスの要素をポインタでの参照にしたいため。これを書かないと、生成されるスライスBookSlice[]Bookとなり、書くと[]*Bookとなる。

生成する

gen

型名_slice.genというファイルが生成される。中には型名Sliceという型と、メソッドWhereが定義されている。

// BookSlice is a slice of type *Book. Use it where you would use []*Book.
type BookSlice []*Book

// Where returns a new BookSlice whose elements return true for func. See: http://clipperhouse.github.io/gen/#Where
func (rcv BookSlice) Where(fn func(*Book) bool) (result BookSlice) {
    for _, v := range rcv {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

非常にシンプルで難しいこと、複雑なことは何もやっていない。これだけならgenを使わなくても手動で書いても問題ないが、Where以外の関数も使い始めるようになったり、あるいは色々な型でこれらの関数を使うようになってくると、やはりコード生成の便利さを感じるようになると思う。

使う

シンプルなコードなので説明するまでもないが、生成されたWhereメソッドを実際に使ってみる。

books := BookSlice{
    {Author: "John Smith", Title: "My life"},
    {Author: "John Smith", Title: "Money"},
    {Author: "Paul Brown", Title: "Your life"},
}

result := books.Where(func(b *Book) bool {
    return b.Author == "John Smith"
})

このWhere関数は、この型(Book)のスライスの関数なので、ビルド時の型チェックが有効であり、また返り値もinterface{}型のスライスではなく、オリジナルの型のスライスとなる。

生成されたコードをさらに拡張する

これはgenの機能ではないのだが、genSlice typewriterで生成されたSliceに、さらに自前で使いたいメソッドを追加で実装するとより便利になる。

自分の場合は型名_slice_ext.goというファイルを作成し、その中で型Sliceに対してさらに良く使う機能を関数で実装している。

func (rcv BookSlice) Titles() []string {
    titles := make([]string, len(rcv))
    for i, b := range rcv {
        titles[i] = b.Title
    }
    return titles
}

こうすることで、genが生成するメソッドと組み合わせて下記のような書き方ができる。

titles := books.Where(func(b *Book) bool {
    return b.Author == "John Smith"
}).Titles()

型チェックが効き、それでいて読みやすくGo的で気に入っている。

結論

コード生成については好き嫌いが分かれるところだが、Goの言語自体にコード生成を補助する機構が用意されていることや、静的解析によるコンパイル時の型チェックの有効性などを考えると、genslice typewriterを使うという選択肢は割とありだと思う。実際に、仕事で多用するようになってから、go-funkを多用していた頃よりもコードが読みやすく書きやすくなったと感じている。エディタからショートカットでgenコマンドを呼び出せるようにしておいたりすると、ますます便利なのでぜひ試してみてほしい。

49
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
26