はじめに
本記事は Go Advent Calendar 2022 の18日目の記事です。
Generics を使った関数でやりたかったことが、samber/lo に大体揃ってたという話をします。
すでに自前で書いてた関数もいくつかあったのですが、このライブラリを知り置き換えていったという体験談です。
※結果的に「samber/lo のなかでこの辺り使えるかも」というものを私の独断と偏見でピックアップする記事になってます。
samber/lo とは
samber/lo は 16日目のアベンドカレンダーの記事 でも取り上げられていまして、そこから説明を引用させていただきました。
samber/loは,多種多様な便利関数を提供するパッケージです。
JavaScriptのライブラリのLodashライクであるとREADMEにも記載があります。
個人的には大好きなパッケージでして,Map,Filter等のスライスを操作する関数をよく使っています。https://github.com/samber/lo
https://github.com/lodash/lodash
計測時点(2022/12/17)でスター数は驚異の 9.1k! last commit は当日! 今後のメンテやアップデートも期待できるライブラリです。
Genericsでやりたかったこと
map の keys/values
あった。
lo.Keys: https://pkg.go.dev/github.com/samber/lo#Keys
playgound: https://go.dev/play/p/Uu11fHASqrU
lo.Values: https://pkg.go.dev/github.com/samber/lo#Values
playgound: https://go.dev/play/p/nnRTQkzQfF6
min/max
あった。
lo.Min: https://pkg.go.dev/github.com/samber/lo#Min
lo.Max: https://pkg.go.dev/github.com/samber/lo#Max
func main() {
	fmt.Println(lo.Min([]int{9, 8, 2, 1, 4, 5}))
	fmt.Println(lo.Max([]int{9, 8, 2, 1, 4, 5}))
	// Output:
	// 1
	// 9
}
(配列じゃなくて可変引数だったら最高だったな)
Must(エラーだったらpanic)
エラーだったらpanic。そうじゃなかったらerr以外の引数をreturnするもの。
test の前処理とかのpanicでもいい時に使いたくなる。
あった。引数はMust6まで定義されてた。
lo.Must1: https://pkg.go.dev/github.com/samber/lo#Must1
GoDoc の playground がわかりづらかったので自分でも書いてみた
func main() {
	fmt.Println(lo.Must1(returnNoErr())) // 1を出力
	fmt.Println(lo.Must1(returnErr()))   // panicする
}
func returnErr() (int, error) {
	return 0, errors.New("error")
}
func returnNoErr() (int, error) {
	return 1, nil
}
値→ポインタ 変換
test の時に超絶欲しくなるやつ。
WebAPI実装時に swag.String, swag.Int64 とか書いてた人も多いのではないでしょうか。
あった。
lo.ToPtr: https://pkg.go.dev/github.com/samber/lo#ToPtr
lo.ToSlicePtr: https://pkg.go.dev/github.com/samber/lo#ToSlicePtr
↓こんな感じ。ValueObjectを導入しているチームも嬉しいはず。
type ValueObject string
type PointerFieldStruct struct {
	Str *string
	VO  *ValueObject
}
func main() {
	p := PointerFieldStruct{
		Str: lo.ToPtr("foo"),
		VO:  lo.ToPtr(ValueObject("bar")),
	}
	// こう書くことが多かった
	// str:= "foo"
	// vo := ValueObject("bar")
	// p := PointerFieldStruct{
	// 	Str: &str,
	// 	VO:  &vo,
	// }
	fmt.Println(*p.Str)
	fmt.Println(*p.VO)
	// Output:
	// foo
	// bar
}
ポインタ→値 変換(nil時のオプショナル付き)
nilだったら、ゼロ値もしくは指定した値を書くもの。
あった。
lo.FromPtr: https://pkg.go.dev/github.com/samber/lo#FromPtr
lo.FromPtrOr:https://pkg.go.dev/github.com/samber/lo#FromPtrOr
func main() {
	var str *string
	fmt.Println(lo.FromPtr(str)) // ゼロ値。つまり空文字。
	fmt.Println(lo.FromPtrOr(str, "別の値"))
	// Output:
	// 
	// 別の値
}
slice の重複排除
あった。
lo.Uniq: https://pkg.go.dev/github.com/samber/lo#Uniq
playground: https://go.dev/play/p/DTzbeXZ6iEN
slice を一定数ごとにsplit
IDの一覧だけ取得して、そのあと一定数ごとにループ処理を回したい時に使いたくなったもの。
あった。
lo.Chunk: https://pkg.go.dev/github.com/samber/lo#Chunk
playground: https://go.dev/play/p/EeKl0AuTehH
slice を逆順に
あった。
lo.Reverse: https://pkg.go.dev/github.com/samber/lo#Reverse
playground: https://go.dev/play/p/fhUMLvZ7vS6
slice に一つでも条件に当てはまるものがあったら
早期return したり、continue したり、ループを抜けるためになにかしたりというのが必要な時に欲しかった。
(Generics じゃなくて堅実に書いていきたいという人もいそうな気もする)
あった。
lo.Contains: https://pkg.go.dev/github.com/samber/lo#Contains
lo.ContainsBy: https://pkg.go.dev/github.com/samber/lo#ContainsBy
lo.Some: https://pkg.go.dev/github.com/samber/lo#Some
lo.SomeBy: https://pkg.go.dev/github.com/samber/lo#SomeBy
(ContainsByとSomeByの実装同じだった)
type Order struct {
	Name   string
	Status string
}
func ActiveStatus() []string {
	return []string{
		"not_yet",
		"work_in_progress",
	}
}
func main() {
	orders := []Order{
		{"1", "not_yet"},
		{"2", "work_in_progress"},
		{"3", "done"},
		{"4", "canceled"},
	}
	for _, o := range orders {
		if lo.Contains(ActiveStatus(), o.Status) {
			fmt.Println(o)
		}
	}
	// Output:
	// {1 not_yet}
	// {2 work_in_progress}
}
type Team struct {
	Name    string
	Members []Person
}
type Person struct {
	Name  string
	Cards []int
}
func main() {
	teams := []Team{
		{
			Name: "A",
			Members: []Person{
				{"bubu", []int{1, 2, 3}},
				{"suke", []int{2, 3, 4}},
			},
		},
		{
			Name: "B",
			Members: []Person{
				{"foo", []int{1, 2, 3}},
				{"bar", []int{4, 5, 6}},
			},
		},
		{
			Name: "C",
			Members: []Person{
				{"hoge", []int{7, 8, 9}},
			},
		},
	}
	// 4 のカードを持っているTeamを出力
	const key = 4
	targets := make([]Team, 0, len(teams))
	// ここでつかってます
	for _, team := range teams {
		if lo.ContainsBy(team.Members, func(member Person) bool {
			return lo.Contains(member.Cards, key)
		}) {
			targets = append(targets, team)
		}
	}
	// こう書くことが多かった
	// for _, team := range teams {
	// 	find := false
	// 	for _, member := range team.Members {
	// 		for _, card := range member.Cards {
	// 			if card == key {
	// 				targets = append(targets, team)
	// 				find = true
	// 				break
	// 			}
	// 		}
	// 		if find {
	// 			break
	// 		}
	// 	}
	// }
	for _, t := range targets {
		fmt.Println(t.Name)
	}
	// Output:
	// A
	// B
}
※上記のcontains_by.goのサンプルコードだとループのなかで fmt.Println すれば良いですが、「N+1問題を避けるために後続で一括処理する」などのケースを想定してもらえるとしっくりくるかなと思います。
データモデル系
なかった。
queue, set などはなかったので自前実装にしてます。
本記事の内容からは少し外れるのでざっくりどう作ってるかだけ記載します。
(Generics を試してみたい方は queue, set ぜひ実装してみてください!)
queue: sliceで実装した記事 を参考につくってます。Push, Top, Pop, IsEmpty, Len あたりのメソッドを持たせてます。型定義だけ書いておきます。
type Queue[T any] struct {
	queue []T
}
set: type Set[T comparable] map[T]Unit を定義して使ってます。setを型定義した方が目的が明確になると思って使ってます。Has, Put, Delete, Merge, ToSlice のメソッドと、SliceToSet という変換関数を書いたりしてます。
おわりに
あったらいいのが揃ってました。
Generics で書くと楽かも
→ でも車輪の再発明が云々
→ でも再発明かどうかライブラリを探すこと自体が時間かかる云々
みたいな悩みが解消すればと思います。
