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

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

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

Начать курс

Библиотека mo для Go: Монады и функциональные паттерны

Добро пожаловать в мир функционального программирования на Go! Библиотека mo (от monads) — это мощный инструмент, который привносит в императивный язык Go концепции монад и функциональных паттернов, популярных в Haskell, Scala и Rust. Если вы когда-нибудь сталкивались с проблемой обработки ошибок через if err != nil, бесконечных вложенных проверок на nil или громоздких цепочек вызовов, mo предлагает элегантное решение.

Основная цель библиотеки — предоставить типобезопасные контейнеры (Option, Result, Either), которые инкапсулируют возможные состояния (наличие/отсутствие значения, успех/ошибка) и позволяют работать с ними в функциональном стиле. Вместо того чтобы писать десятки условных операторов, вы строите цепочки трансформаций, где каждый шаг обрабатывается автоматически. Это делает код более читаемым, предсказуемым и менее подверженным ошибкам, особенно в сложных бизнес-логиках.

Библиотека mo не пытается превратить Go в Haskell. Она предлагает прагматичный подход: вы используете функциональные паттерны там, где это действительно упрощает код, сохраняя при этом привычный синтаксис Go. mo особенно полезна для обработки ошибок, работы с опциональными значениями, композиции функций и реализации пайплайнов обработки данных. Она хорошо сочетается с горутинами и каналами, позволяя строить надежные конкурентные программы.

Установка

Установка библиотеки mo происходит стандартным для Go способом через менеджер пакетов go get. Выполните следующую команду в терминале в корне вашего проекта (там, где находится go.mod):

go get github.com/samber/mo

После выполнения этой команды библиотека будет добавлена в ваш go.mod файл, и вы сможете импортировать её в свои исходные файлы. Убедитесь, что у вас установлена версия Go 1.18 или выше (требуется для поддержки дженериков).

Быстрый старт

Давайте начнем с классического примера — безопасного деления двух чисел. В традиционном Go мы бы вернули ошибку. С mo мы используем тип Result, который может быть либо Ok (успех), либо Err (ошибка).

package main

import (
    "fmt"
    "github.com/samber/mo"
)

// safeDivide возвращает Result, который содержит либо результат деления, либо ошибку.
func safeDivide(a, b float64) mo.Result[float64] {
    if b == 0 {
        return mo.Err[float64](fmt.Errorf("деление на ноль"))
    }
    return mo.Ok(a / b)
}

func main() {
    result := safeDivide(10, 2)

    // Используем метод Match для обработки обоих случаев.
    result.Match(
        func(value float64) {
            fmt.Printf("Результат: %.2f\n", value)
        },
        func(err error) {
            fmt.Printf("Ошибка: %s\n", err)
        },
    )

    // Попробуем деление на ноль.
    result2 := safeDivide(10, 0)
    result2.Match(
        func(value float64) {
            fmt.Printf("Результат: %.2f\n", value)
        },
        func(err error) {
            fmt.Printf("Ошибка: %s\n", err)
        },
    )
}

Этот пример демонстрирует базовую идею: вместо того чтобы проверять ошибку вручную, мы передаем ответственность за обработку библиотеке, используя метод Match.

Основные методы и функции

Ниже представлены ключевые компоненты библиотеки mo. Мы рассмотрим 8 основных методов/типов, которые покроют 90% ваших потребностей.

1. mo.Ok[T any] и mo.Err[T any]

Сигнатура:

  • func Ok[T any](value T) Result[T]
  • func Err[T any](err error) Result[T]

Что делает: Создают экземпляры типа Result[T]. Ok представляет успешное выполнение операции, содержащее значение типа T. Err представляет ошибку, содержащую объект error.

package main

import (
    "errors"
    "fmt"
    "github.com/samber/mo"
)

func main() {
    success := mo.Ok(42)
    failure := mo.Err[int](errors.New("что-то пошло не так"))

    fmt.Println(success.IsOk())  // true
    fmt.Println(failure.IsErr()) // true
}

2. Result.Match

Сигнатура:

  • func (r Result[T]) Match(onSuccess func(T), onFailure func(error))

Что делает: Выполняет один из двух переданных коллбэков в зависимости от состояния Result. Если это Ok, вызывается onSuccess со значением. Если Err — вызывается onFailure с ошибкой. Это основной способ обработки результата.

package main

import (
    "errors"
    "fmt"
    "github.com/samber/mo"
)

func main() {
    result := mo.Ok("Hello, World!")
    result.Match(
        func(v string) { fmt.Println("Успех:", v) },
        func(err error) { fmt.Println("Ошибка:", err) },
    )
    // Вывод: Успех: Hello, World!

    result2 := mo.Err[string](errors.New("ошибка"))
    result2.Match(
        func(v string) { fmt.Println("Успех:", v) },
        func(err error) { fmt.Println("Ошибка:", err) },
    )
    // Вывод: Ошибка: ошибка
}

3. Result.Map

Сигнатура:

  • func (r Result[T]) Map(f func(T) U) Result[U]

Что делает: Трансформирует значение внутри Result, если он в состоянии Ok. Если Result — это Err, то трансформация не применяется, и ошибка "пробрасывается" дальше. Позволяет строить цепочки преобразований без проверки ошибок на каждом шагу.

package main

import (
    "fmt"
    "github.com/samber/mo"
    "strconv"
)

func main() {
    // Начинаем с Ok("123")
    result := mo.Ok("123").
        Map(func(s string) int {
            i, _ := strconv.Atoi(s)
            return i
        }).
        Map(func(i int) string {
            return fmt.Sprintf("Число: %d", i)
        })

    result.Match(
        func(v string) { fmt.Println(v) }, // Вывод: Число: 123
        func(err error) { fmt.Println(err) },
    )
}

4. Result.FlatMap

Сигнатура:

  • func (r Result[T]) FlatMap(f func(T) Result[U]) Result[U]

Что делает: Похож на Map, но функция f сама возвращает Result[U]. Это необходимо, когда трансформация может завершиться ошибкой. FlatMap "разворачивает" вложенный Result, предотвращая появление Result[Result[U]].

package main

import (
    "fmt"
    "github.com/samber/mo"
    "strconv"
)

func parseInt(s string) mo.Result[int] {
    i, err := strconv.Atoi(s)
    if err != nil {
        return mo.Err[int](err)
    }
    return mo.Ok(i)
}

func main() {
    result := mo.Ok("42").
        FlatMap(parseInt).
        Map(func(i int) string {
            return fmt.Sprintf("Квадрат: %d", i*i)
        })

    result.Match(
        func(v string) { fmt.Println(v) }, // Вывод: Квадрат: 1764
        func(err error) { fmt.Println(err) },
    )

    // Пример с ошибкой
    result2 := mo.Ok("abc").
        FlatMap(parseInt).
        Map(func(i int) string {
            return fmt.Sprintf("Квадрат: %d", i*i)
        })

    result2.Match(
        func(v string) { fmt.Println(v) },
        func(err error) { fmt.Println("Ошибка:", err) }, // Вывод: Ошибка: strconv.Atoi: parsing "abc": invalid syntax
    )
}

5. mo.Some[T any] и mo.None[T any]

Сигнатура:

  • func Some[T any](value T) Option[T]
  • func None[T any]() Option[T]

Что делает: Создают экземпляры типа Option[T]. Some представляет наличие значения, None — его отсутствие. Это безопасная альтернатива указателям, которые могут быть nil.

package main

import (
    "fmt"
    "github.com/samber/mo"
)

func findUser(id int) mo.Option[string] {
    if id == 1 {
        return mo.Some("Alice")
    }
    return mo.None[string]()
}

func main() {
    user := findUser(1)
    user.Match(
        func(name string) { fmt.Println("Найден:", name) },
        func() { fmt.Println("Пользователь не найден") },
    ) // Вывод: Найден: Alice

    user2 := findUser(2)
    user2.Match(
        func(name string) { fmt.Println("Найден:", name) },
        func() { fmt.Println("Пользователь не найден") },
    ) // Вывод: Пользователь не найден
}

6. Option.Map

Сигнатура:

  • func (o Option[T]) Map(f func(T) U) Option[U]

Что делает: Трансформирует значение внутри Option, если он в состоянии Some. Если Option — None, трансформация не применяется.

package main

import (
    "fmt"
    "github.com/samber/mo"
)

func main() {
    option := mo.Some(10).
        Map(func(i int) int { return i * 2 }).
        Map(func(i int) string { return fmt.Sprintf("Результат: %d", i) })

    option.Match(
        func(v string) { fmt.Println(v) }, // Вывод: Результат: 20
        func() { fmt.Println("Значение отсутствует") },
    )

    none := mo.None[int]().
        Map(func(i int) int { return i * 2 })

    none.Match(
        func(v int) { fmt.Println(v) },
        func() { fmt.Println("Значение отсутствует") }, // Вывод: Значение отсутствует
    )
}

7. Option.FlatMap

Сигнатура:

  • func (o Option[T]) FlatMap(f func(T) Option[U]) Option[U]

Что делает: Аналог Result.FlatMap, но для Option. Используется, когда функция трансформации сама может вернуть None.

package main

import (
    "fmt"
    "github.com/samber/mo"
)

func getDiscount(price float64) mo.Option[float64] {
    if price > 100 {
        return mo.Some(price * 0.9) // 10% скидка
    }
    return mo.None[float64]()
}

func main() {
    finalPrice := mo.Some(150.0).
        FlatMap(getDiscount).
        Map(func(p float64) string {
            return fmt.Sprintf("Цена со скидкой: %.2f", p)
        })

    finalPrice.Match(
        func(v string) { fmt.Println(v) }, // Вывод: Цена со скидкой: 135.00
        func() { fmt.Println("Скидка не применима") },
    )

    finalPrice2 := mo.Some(50.0).
        FlatMap(getDiscount).
        Map(func(p float64) string {
            return fmt.Sprintf("Цена со скидкой: %.2f", p)
        })

    finalPrice2.Match(
        func(v string) { fmt.Println(v) },
        func() { fmt.Println("Скидка не применима") }, // Вывод: Скидка не применима
    )
}

8. Either.Left и Either.Right

Сигнатура:

  • func Left[L any, R any](value L) Either[L, R]
  • func Right[L any, R any](value R) Either[L, R]

Что делает: Тип Either[L, R] представляет значение одного из двух возможных типов. Обычно Left используется для ошибки, а Right — для успеха (следуя соглашению из Haskell). Это более общая альтернатива Result, где "ошибка" может быть любого типа, а не только error.

package main

import (
    "fmt"
    "github.com/samber/mo"
)

func validateAge(age int) mo.Either[string, int] {
    if age >= 18 {
        return mo.Right[string, int](age)
    }
    return mo.Left[string, int]("Возраст меньше 18")
}

func main() {
    result := validateAge(25)
    result.Match(
        func(errMsg string) { fmt.Println("Ошибка:", errMsg) },
        func(age int) { fmt.Println("Возраст:", age) },
    ) // Вывод: Возраст:                    

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