lo для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Библиотека 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)
    // Вывод: Просро                    

Рекомендаций