👻

[Go] samber/lo が便利なのは Find/Filter/Map だけじゃない!

2023/10/11に公開

はじめに

2022 年 3 月にリリースされた Go 1.18 からジェネリクスが使えるようになりました。ジェネリクスが使えるようになって嬉しかったことのひとつに、これまで for 文でゴリゴリ書いていた Filter/Find/Map のようなスライスの扱いをより簡潔に書けるようになったことがあります。samber/lo にはジェネリクスを使った汎用的なスライス操作が実装されており、その紹介記事も散見されます。

https://github.com/samber/lo

Filter/Find/Map などの他にもジェネリクスを活用した便利なユーティリティーが samber/lo にはたくさん実装されています。ですが、私は最近コードレビューで「nits: samber/lo を使うとちょっと楽に書けますよ」的なコメントを残すことが多く、その便利さがそこまで知られていないように思います。本記事では私の観測範囲内でこのパッケージの便利さを布教したいと思います。

便利な関数たち

EveryBy/SomeBy

JavaScript の Array.prototype.everyArray.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 ご覧ください。何だかスマートに処理を書きたくなってきたな、と思ったらこちらのドキュメントを見に行ってもらえると良いかと思います。

https://pkg.go.dev/github.com/samber/lo

いつしか samber/lo の機能が標準パッケージに取り込まれていることを夢見ています。

GitHubで編集を提案
株式会社BuySell Technologies

Discussion