github.com/samber/lo の導入を進めている話

ソフトウェアエンジニアの北原です。

Nature Remo Advent Calendar 2022 の1日目です。(Nature の中の人はもちろん、Nature Remo の活用方法や cloud / Local API を使ってこんなツール作りました、まで Nature Remo に関する話題でしたら何でも歓迎です。 ぜひご参加ください。)

adventar.org

Nature のバックエンドは Go でおおよそが書かれています。Go 1.18 で Generics が導入されて以降少しずつライブラリは自ら Generics を利用しています。 今回はその中でも一番使用している github.com/samber/lo について紹介します。

github.com

github.com/samber/lo は lodash ライクな関数を Go の Generics を使用して提供しているライブラリになりまして、所謂 Slice や Map に対する処理を簡潔に記述することができます。

以前の Go とだいぶ書き味が違う点もあり賛否両論なところもあるかもしれませんが、Nature では簡潔に記述できて、コードレビューなどでもループ処理に対して意図が伝わりやすいなどでよく利用するようになりました。

プロダクションのコードでよく使われている関数を以下でした。 サンプルのコードは https://github.com/samber/lo を参照させて頂きます。

1位 lo.Map

lo.Map([]int64{1, 2, 3, 4}, func(x int64, index int) string {
    return strconv.FormatInt(x, 10)
})
// []string{"1", "2", "3", "4"}

2位の5倍以上利用されていました。Slice を何かに変換する、というのはまぁよくある処理ですから、そういうものでしょう。

2位 lo.GroupBy

groups := lo.GroupBy([]int{0, 1, 2, 3, 4, 5}, func(i int) int {
    return i%3
})
// map[int][]int{0: []int{0, 3}, 1: []int{1, 4}, 2: []int{2, 5}}

こちらは Slice からキーとなる値を返し、グループ化する関数です。 Nature では DB からレコードを取得した後に何か元にグループに分ける場合に使われていました。

3位 lo.KeyBy

m := lo.KeyBy([]string{"a", "aa", "aaa"}, func(str string) int {
    return len(str)
})
// map[int]string{1: "a", 2: "aa", 3: "aaa"}

こちらは GroupBy と似ていますが、map のヴァリューとなる値は Slice ではなく、値となります。 使用用途も Group By と似ていますが、ユニークなカラムが存在するレコード郡を取得したのちに、そのカラムをキーとした Map を作成しておき、その後使用するケースが多いようでした。

3位 lo.Filter

even := lo.Filter([]int{1, 2, 3, 4}, func(x int, index int) bool {
    return x%2 == 0
})
// []int{2, 4}

同率で3位でした。まぁこれもよくある処理ですね。


こちら実はプロダクションのテストを含まないコードを対象にしたのですが、テストコードを対象にすると lo.MustX がよく使われていました。

val := lo.Must(time.Parse("2006-01-02", "2022-01-15"))
// 2022-01-15

テストコードにおいて、丁寧にエラーハンドリングを書かなくても良いようなケースでよく使われています。


github.com/samber/lo の中でも面白いなと思うのは Ternary という関数でこちらを利用すると三項演算子のように書くことができます。 ここまでくると少し本来の Go の書き味と違いすぎる...というのもあるかもしれません。Nature のコードではこちらの関数は利用していませんでした。

result := lo.Ternary(true, "a", "b")
// "a"

その他、独自に作っている Slice のための Generics を利用した関数としては

// MapOrError is a verion of lo.Map that can return an error.
func MapOrError[T any, R any](collection []T, iteratee func(T, int) (R, error)) ([]R, error) {
    result := make([]R, len(collection))
    var err error

    for i, item := range collection {
        result[i], err = iteratee(item, i)
        if err != nil {
            return nil, err
        }
    }

    return result, nil
}

このようなものがあります。こちらを利用すると、lo.Map のように処理をしますが、渡した関数がエラーを返した場合は直ちに処理をとめエラーを返す、というものです。 以前は他にももう少しあったのですが、github.com/samber/lo に継続的に関数が追加されている結果、Nature 側のコードで実装する必要がなくなっていきました。

エンジニア積極採用中です

Natureでは一緒に開発してくれる仲間を募集しています。

herp.careers

カジュアル面談も歓迎なので、ぜひお申し込みください。

herp.careers

Natureのミッション、サービス、組織や文化、福利厚生についてご興味のある方は、ぜひCulture Deckをご覧ください。

speakerdeck.com