Библиотека lo для Go: Дженерик-утилиты (Lodash для Go)
В мире разработки на Go часто возникает потребность в удобных функциях для работы с коллекциями, которые есть в других языках (например, map, filter, reduce из JavaScript или Python). Стандартная библиотека Go предоставляет минимальный набор инструментов, и написание однотипных циклов for для каждой операции становится утомительным. Именно здесь на помощь приходит библиотека lo — мощный набор дженерик-утилит, вдохновлённый популярной JavaScript-библиотекой Lodash.
Библиотека lo использует дженерики (обобщённые типы), появившиеся в Go 1.18+, чтобы предоставлять типобезопасные функции для работы со слайсами, мапами, каналами и другими структурами данных. Вместо того чтобы писать for i := range slice { ... } для каждой трансформации, вы можете использовать одну строку кода: lo.Map(slice, transformFunc). Это делает код более читаемым, лаконичным и менее подверженным ошибкам.
Зачем использовать lo? Во-первых, это экономия времени: вы перестаёте писать boilerplate-код. Во-вторых, это повышение надёжности: функции библиотеки тщательно протестированы и покрывают множество edge-кейсов. В-третьих, это единообразие: вся команда использует одни и те же функции с понятными названиями, что упрощает код-ревью. lo — это must-have для любого Go-проекта, где активно используются коллекции данных.
Установка
Установка библиотеки lo производится стандартным для Go способом через go get. Убедитесь, что у вас установлена версия Go 1.18 или выше (так как библиотека использует дженерики).
go get github.com/samber/lo@latest
После выполнения этой команды библиотека будет добавлена в ваш go.mod файл. Теперь вы можете импортировать её в свой код:
import "github.com/samber/lo"
Быстрый старт
Давайте напишем простейшую программу, которая использует lo для фильтрации и трансформации слайса чисел.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
// Исходный слайс чисел
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// Фильтруем только чётные числа
evenNumbers := lo.Filter(numbers, func(x int, _ int) bool {
return x%2 == 0
})
fmt.Println("Чётные числа:", evenNumbers) // [2 4 6 8 10]
// Удваиваем каждое число
doubled := lo.Map(evenNumbers, func(x int, _ int) int {
return x * 2
})
fmt.Println("Удвоенные чётные числа:", doubled) // [4 8 12 16 20]
// Суммируем все числа
sum := lo.Sum(doubled)
fmt.Println("Сумма:", sum) // 60
}
Как видите, код стал гораздо более декларативным и читаемым. Мы избежали написания трёх отдельных циклов for.
Основные методы и функции
Ниже приведён список ключевых функций библиотеки lo с примерами использования. Большинство функций принимают слайс и функцию обратного вызова (callback).
1. lo.Filter
Сигнатура: func Filter[T any](collection []T, predicate func(item T, index int) bool) []T
Что делает: Создаёт новый слайс, содержащий только те элементы исходного слайса, для которых функция-предикат вернула true.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
names := []string{"Alice", "Bob", "Charlie", "David", "Eve"}
// Оставляем только имена длиннее 3 символов
longNames := lo.Filter(names, func(name string, _ int) bool {
return len(name) > 3
})
fmt.Println(longNames) // [Alice Charlie David]
}
2. lo.Map
Сигнатура: func Map[T any, R any](collection []T, iteratee func(item T, index int) R) []R
Что делает: Трансформирует каждый элемент слайса, применяя к нему функцию-итератор, и возвращает новый слайс с результатами.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
prices := []float64{10.5, 20.0, 30.75}
// Добавляем налог 10% к каждой цене
pricesWithTax := lo.Map(prices, func(price float64, _ int) float64 {
return price * 1.1
})
fmt.Println(pricesWithTax) // [11.55 22 33.825]
}
3. lo.Reduce
Сигнатура: func Reduce[T any, R any](collection []T, accumulator func(agg R, item T, index int) R, initial R) R
Что делает: Последовательно применяет функцию-аккумулятор к каждому элементу слайса, накапливая результат. Начинает с начального значения initial.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
numbers := []int{1, 2, 3, 4, 5}
// Вычисляем произведение всех чисел
product := lo.Reduce(numbers, func(agg int, item int, _ int) int {
return agg * item
}, 1) // начальное значение = 1
fmt.Println(product) // 120
}
4. lo.Find
Сигнатура: func Find[T any](collection []T, predicate func(item T) bool) (T, bool)
Что делает: Ищет первый элемент в слайсе, удовлетворяющий условию предиката. Возвращает найденный элемент и true, или нулевое значение и false, если элемент не найден.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
ages := []int{25, 30, 17, 40, 22}
// Ищем первого совершеннолетнего (возраст >= 18)
adult, found := lo.Find(ages, func(age int) bool {
return age >= 18
})
if found {
fmt.Println("Первый совершеннолетний:", adult) // 25
}
}
5. lo.Uniq
Сигнатура: func Uniq[T comparable](collection []T) []T
Что делает: Удаляет дубликаты из слайса, сохраняя порядок первых вхождений. Работает только с сопоставимыми (comparable) типами.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
ids := []int{1, 2, 2, 3, 1, 4, 5, 3}
uniqueIds := lo.Uniq(ids)
fmt.Println(uniqueIds) // [1 2 3 4 5]
}
6. lo.GroupBy
Сигнатура: func GroupBy[T any, K comparable](collection []T, iteratee func(item T) K) map[K][]T
Что делает: Группирует элементы слайса по ключу, который вычисляется функцией-итератором. Возвращает мапу, где ключ — это результат итератора, а значение — слайс элементов с этим ключом.
package main
import (
"fmt"
"github.com/samber/lo"
)
type Person struct {
Name string
Age int
}
func main() {
people := []Person{
{"Alice", 25},
{"Bob", 30},
{"Charlie", 25},
{"David", 30},
}
// Группируем людей по возрасту
grouped := lo.GroupBy(people, func(p Person) int {
return p.Age
})
fmt.Println(grouped)
// map[25:[{Alice 25} {Charlie 25}] 30:[{Bob 30} {David 30}]]
}
7. lo.Chunk
Сигнатура: func Chunk[T any](collection []T, size int) [][]T
Что делает: Разбивает слайс на более мелкие слайсы (чанки) указанного размера. Последний чанк может быть меньше, если количество элементов не кратно размеру.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
data := []int{1, 2, 3, 4, 5, 6, 7}
// Разбиваем на чанки по 3 элемента
chunks := lo.Chunk(data, 3)
fmt.Println(chunks) // [[1 2 3] [4 5 6] [7]]
}
8. lo.Flatten
Сигнатура: func Flatten[T any](collection [][]T) []T
Что делает: «Схлопывает» вложенный слайс слайсов в один одномерный слайс.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
matrix := [][]int{{1, 2}, {3, 4, 5}, {6}}
flat := lo.Flatten(matrix)
fmt.Println(flat) // [1 2 3 4 5 6]
}
9. lo.PartitionBy
Сигнатура: func PartitionBy[T any, K comparable](collection []T, iteratee func(item T) K) [][]T
Что делает: Разделяет слайс на последовательные группы, где функция-итератор возвращает одинаковое значение. Отличается от GroupBy тем, что сохраняет порядок и не объединяет разрозненные группы с одинаковым ключом.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
items := []int{1, 1, 2, 2, 2, 3, 1, 1}
// Разделяем на группы одинаковых чисел
partitioned := lo.PartitionBy(items, func(x int) int {
return x
})
fmt.Println(partitioned) // [[1 1] [2 2 2] [3] [1 1]]
}
10. lo.PickBy (для мап)
Сигнатура: func PickBy[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) map[K]V
Что делает: Создаёт новую мапу, содержащую только те пары ключ-значение, для которых функция-предикат вернула true.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
scores := map[string]int{"Alice": 85, "Bob": 92, "Charlie": 78, "David": 95}
// Оставляем только тех, у кого балл выше 80
highScores := lo.PickBy(scores, func(_ string, score int) bool {
return score > 80
})
fmt.Println(highScores) // map[Alice:85 Bob:92 David:95]
}
Реальный практический пример
Представьте, что вы пишете микросервис для обработки заказов. Вам нужно получить список заказов из БД, отфильтровать просроченные, сгруппировать их по статусу, а затем отправить уведомления. Использование lo сделает этот код элегантным и понятным.
package main
import (
"fmt"
"time"
"github.com/samber/lo"
)
// Order представляет заказ.
type Order struct {
ID int
Status string // "pending", "shipped", "delivered", "cancelled"
Amount float64
CreatedAt time.Time
}
// Notification представляет уведомление.
type Notification struct {
OrderID int
Message string
}
func main() {
// Имитация данных из БД
orders := []Order{
{ID: 1, Status: "pending", Amount: 150.0, CreatedAt: time.Now().Add(-48 * time.Hour)},
{ID: 2, Status: "shipped", Amount: 200.0, CreatedAt: time.Now().Add(-24 * time.Hour)},
{ID: 3, Status: "pending", Amount: 75.0, CreatedAt: time.Now().Add(-72 * time.Hour)},
{ID: 4, Status: "delivered", Amount: 300.0, CreatedAt: time.Now().Add(-12 * time.Hour)},
{ID: 5, Status: "cancelled", Amount: 50.0, CreatedAt: time.Now().Add(-96 * time.Hour)},
{ID: 6, Status: "pending", Amount: 120.0, CreatedAt: time.Now().Add(-1 * time.Hour)},
}
// 1. Фильтруем просроченные заказы (статус "pending" и созданы более 48 часов назад)
staleOrders := lo.Filter(orders, func(o Order, _ int) bool {
return o.Status == "pending" && time.Since(o.CreatedAt) > 48*time.Hour
})
fmt.Printf("Просроченные заказы (%d): %+v\n", len(staleOrders), staleOrders)
// Вывод: Просро Также в библиотеке
Машинное обучение на C# (.NET): обзор библиотек ML.NET, Accord.NET, TensorFlow.NET
Log4j 2 / SLF4J для Java: полное руководство с примерами
Отправка SMS на PHP: Обзор API библиотек для интеграции
Rate Limiting в TypeScript: библиотеки, стратегии и примеры
JSON обработка в Java: библиотеки Jackson и Gson для работы с данными