[Go] samber/lo が便利なのは Find/Filter/Map だけじゃない!
はじめに
2022 年 3 月にリリースされた Go 1.18 からジェネリクスが使えるようになりました。ジェネリクスが使えるようになって嬉しかったことのひとつに、これまで for 文でゴリゴリ書いていた Filter/Find/Map のようなスライスの扱いをより簡潔に書けるようになったことがあります。samber/lo
にはジェネリクスを使った汎用的なスライス操作が実装されており、その紹介記事も散見されます。
Filter/Find/Map などの他にもジェネリクスを活用した便利なユーティリティーが samber/lo
にはたくさん実装されています。ですが、私は最近コードレビューで「nits: samber/lo
を使うとちょっと楽に書けますよ」的なコメントを残すことが多く、その便利さがそこまで知られていないように思います。本記事では私の観測範囲内でこのパッケージの便利さを布教したいと思います。
便利な関数たち
EveryBy/SomeBy
JavaScript の Array.prototype.every
と Array.prototype.some
に相当する関数です。スライスのすべての要素が条件を満たすかどうかを判定する EveryBy
と、スライスのいずれかの要素が条件を満たすかどうかを判定する SomeBy
があります。
EveryBy([]int{1, 2, 3, 4}, func(x int) bool {
return x < 5
})
// → true
SomeBy([]int{1, 2, 3, 4}, func(x int) bool {
return x < 3
})
// → true
ToPtr/FromPtr
ポインタ型を型安全に扱うことができる関数です。それぞれポインタ型の変数に nil
が入っていたら該当の型のゼロ値を返却する仕様になっています。自分はめちゃくちゃ使います。
type User struct {
Name *string
}
user1 := User{
Name: nil,
}
// こう書いていたのが
if user1.Name == nil && *user1.Name == "" {
println("user1.Name is empty")
}
// こう書ける
if lo.FromPtr(user1.Name) == "" {
println("user1.Name is empty")
}
type User struct {
Name *string
}
// こう書いていたり
userName := "Alice"
user2 := User{
Name: &userName,
}
// こう書いていたのが
user3 := User{
Name: &[]string{"Bob"}[0],
}
// こう書ける
user4 := User{
Name: lo.ToPtr("Carol"),
}
Ternary
三項演算子のような感じで使えます。if 文や即時関数を使って書いていたところが簡潔になります。ちなみに三項演算子のことを ternary operator というようです。(この関数を使って初めて知りました)
v := 10
// こう書いていたり
var msg1 string
if v > 10 {
msg1 = "vは10より大きい"
}
msg1 = "vは10以下"
// こう書いていたのが
msg2 := func() string {
if v > 10 {
return "vは10より大きい"
}
return "vは10以下"
}()
// こう書ける
msg3 := lo.Ternary(v > 10, "vは10より大きい", "vは10以下")
If/ElseIf/Else
Ternary
と同じく、if 文や即時関数を使って書いていたところが簡潔になります。Ternary
と違って 3 つ以上の返り値の分岐を作ることが可能です。Ruby で見られる if 文の戻り値を代入するあのスタイルに近いです。
If
/ElseIf
/Else
はメソッドチェーンでつなげることができます。
v := 10
// こう書いていたのが
var msg1 string
if v > 10 {
msg1 = "vは10より大きい"
} else if v == 10 {
msg1 = "vは10"
} else {
msg1 = "vは10より小さい"
}
// こう書ける
msg2 := lo.If(v > 10, "vは10より大きい").
ElseIf(v == 10, "vは10").
Else("vは10より小さい")
似たような使い心地の Switch
/Case
/Default
というのも用意されています。
GroupBy
特定の条件でスライスをグルーピングすることができます。グループ化された値は map の同じキーにまとめられて返却されます。Web のバックエンドを書いているだけだと SQL の GROUP BY で事足りることが多く、あまり使う機会はない印象ですが、手元の CSV や JSON を加工するのにちょっとしたスクリプトを書くとき等に私はよく使います。
type User struct {
Name string
}
type Task struct {
User User
Title string
}
tasks := []Task{
{User: User{Name: "Alice"}, Title: "Task 1"},
{User: User{Name: "Alice"}, Title: "Task 2"},
{User: User{Name: "Bob"}, Title: "Task 3"},
{User: User{Name: "Bob"}, Title: "Task 4"},
{User: User{Name: "Carol"}, Title: "Task 5"},
}
groupedTasks := lo.GroupBy(tasks, func(item Task) string {
return item.User.Name
})
pretty.Println(groupedTasks)
// map[string][]Task{
// "Alice": {
// {
// User: User{Name:"Alice"},
// Title: "Task 1",
// },
// {
// User: User{Name:"Alice"},
// Title: "Task 2",
// },
// },
// "Bob": {
// {
// User: User{Name:"Bob"},
// Title: "Task 3",
// },
// {
// User: User{Name:"Bob"},
// Title: "Task 4",
// },
// },
// "Carol": {
// {
// User: User{Name:"Carol"},
// Title: "Task 5",
// },
// },
// }
Must
ある関数から error が返っていたら panic してくれるものです。if err != nil { panic(err) }
をいちいち書くのが面倒なときに使います。
元の関数の返り値の個数が 1~7 個の場合まで対応した Must0()
~ Must6
が用意されているので、返り値の個数に合わせてそれぞれを使い分けることができます。
str := "10"
// こう書いていたのが
i1, err := strconv.Atoi(str)
if err != nil {
panic(err)
}
// こう書ける
i2 := lo.Must(strconv.Atoi(str)) // Must は Must1 の alias
まとめ
これ以外にも samber/lo
にはたくさん便利そうな関数が用意されていますので、ぜひ一度 GoDoc ご覧ください。何だかスマートに処理を書きたくなってきたな、と思ったらこちらのドキュメントを見に行ってもらえると良いかと思います。
いつしか samber/lo
の機能が標準パッケージに取り込まれていることを夢見ています。
Discussion