LoginSignup
14

More than 1 year has passed since last update.

Golang の Generics で使いたいものは大体 samber/lo にあった話

Last updated at Posted at 2022-12-17

はじめに

本記事は Go Advent Calendar 2022 の18日目の記事です。

Generics を使った関数でやりたかったことが、samber/lo に大体揃ってたという話をします。
すでに自前で書いてた関数もいくつかあったのですが、このライブラリを知り置き換えていったという体験談です。

※結果的に「samber/lo のなかでこの辺り使えるかも」というものを私の独断と偏見でピックアップする記事になってます。

samber/lo とは

samber/lo16日目のアベンドカレンダーの記事 でも取り上げられていまして、そこから説明を引用させていただきました。

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

min_max.go
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 がわかりづらかったので自分でも書いてみた

must.go
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を導入しているチームも嬉しいはず。

pointer.go
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

from_ptr.go
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の実装同じだった)

contains.go

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}
}
contains_by.go
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 あたりのメソッドを持たせてます。型定義だけ書いておきます。

queue.go
type Queue[T any] struct {
	queue []T
}

set: type Set[T comparable] map[T]Unit を定義して使ってます。setを型定義した方が目的が明確になると思って使ってます。Has, Put, Delete, Merge, ToSlice のメソッドと、SliceToSet という変換関数を書いたりしてます。

おわりに

あったらいいのが揃ってました。

Generics で書くと楽かも
→ でも車輪の再発明が云々
→ でも再発明かどうかライブラリを探すこと自体が時間かかる云々

みたいな悩みが解消すればと思います。

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
14