golang-samber-mo

golang-samber-mo

热门

使用 samber/mo 为 Golang 提供单子类型——Option、Result、Either、Future、IO、Task 和 State 类型,用于类型安全的可空值、错误处理和函数式组合,并包含管道子包。适用于使用或采用 samber/mo 时、代码库导入 `github.com/samber/mo` 时,或考虑将函数式编程模式作为 Golang 安全设计时。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-samber-mo
描述

使用 samber/mo 为 Golang 提供单子类型——Option、Result、Either、Future、IO、Task 和 State 类型,用于类型安全的可空值、错误处理和函数式组合,并包含管道子包。适用于使用或采用 samber/mo 时、代码库导入 `github.com/samber/mo` 时,或考虑将函数式编程模式作为 Golang 安全设计时。

角色: 你是一位将函数式编程安全性引入 Go 的 Go 工程师。你使用单子(monad)使不可能的状态变得不可表示——nil 检查变成类型约束,错误处理变成可组合的管道。

思考模式: 在设计多步骤的 Option/Result/Either 管道时使用 ultrathink。错误的类型选择会导致不必要的包装/解包,从而违背单子的目的。

samber/mo — Go 的单子与函数式抽象

Go 1.18+ 库,提供类型安全的单子类型,零依赖。灵感来自 Scala、Rust 和 fp-ts。

官方资源:

本技能并非详尽无遗。请参考库文档和代码示例获取更多信息。Context7 可作为发现平台提供帮助。对于 Go 包文档、版本、符号和已知漏洞,→ 参见 samber/cc-skills-golang@golang-pkg-go-dev 技能。

go get github.com/samber/mo

有关函数式编程概念以及单子在 Go 中为何有价值的介绍,请参见 单子指南

核心类型一览

类型 目的 将其视为...
Option[T] 可能缺失的值 Rust 的 Option,Java 的 Optional
Result[T] 可能失败的操作 Rust 的 Result<T, E>,替代 (T, error)
Either[L, R] 两种类型之一的值 Scala 的 Either,TypeScript 的判别联合
EitherX[L, R] X 种类型之一的值 Scala 的 Either,TypeScript 的判别联合
Future[T] 尚未可用的异步值 JavaScript 的 Promise
IO[T] 惰性同步副作用 Haskell 的 IO
Task[T] 惰性异步计算 fp-ts 的 Task
State[S, A] 有状态计算 Haskell 的 State 单子

Option[T] — 无 nil 的可空值

表示一个要么存在(Some)要么不存在(None)的值。在类型层面消除 nil 指针风险。

import "github.com/samber/mo"

name := mo.Some("Alice")          // Option[string] 带值
empty := mo.None[string]()        // Option[string] 不带值
fromPtr := mo.PointerToOption(ptr) // nil 指针 -> None

// 安全提取
name.OrElse("Anonymous")  // "Alice"
empty.OrElse("Anonymous")  // "Anonymous"

// 存在时转换,不存在时跳过
upper := name.Map(func(s string) (string, bool) {
    return strings.ToUpper(s), true
})

关键方法: SomeNoneGetMustGetOrElseOrEmptyMapFlatMapMatchForEachToPointerIsPresentIsAbsent

Option 实现了 json.Marshaler/Unmarshalersql.Scannerdriver.Valuer——可直接在 JSON 结构体和数据库模型中使用。

完整 API 参考,请参见 Option 参考

Result[T] — 作为值的错误处理

表示成功(Ok)或失败(Err)。等价于 Either[error, T],但针对 Go 的错误模式进行了特化。

// 包装 Go 的 (value, error) 模式
result := mo.TupleToResult(os.ReadFile("config.yaml"))

// 同类型转换——错误自动短路
upper := mo.Ok("hello").Map(func(s string) (string, error) {
    return strings.ToUpper(s), nil
})
// Ok("HELLO")

// 带默认值提取
val := upper.OrElse("default")

Go 的限制: 直接方法(.Map.FlatMap)无法更改类型参数——Result[T].Map 返回 Result[T],而不是 Result[U]。Go 方法无法引入新的类型参数。对于类型转换(例如 Result[[]byte]Result[Config]),请使用子包函数或 mo.Do

import "github.com/samber/mo/result"

// 类型转换管道:[]byte -> Config -> ValidConfig
parsed := result.Pipe2(
    mo.TupleToResult(os.ReadFile("config.yaml")),
    result.Map(func(data []byte) Config { return parseConfig(data) }),
    result.FlatMap(func(cfg Config) mo.Result[ValidConfig] { return validate(cfg) }),
)

关键方法: OkErrErrfTupleToResultTryGetMustGetOrElseMapFlatMapMapErrMatchForEachToEitherIsOkIsError

完整 API 参考,请参见 Result 参考

Either[L, R] — 两种类型的判别联合

表示两种可能类型之一的值。与 Result 不同,两边都不暗示成功或失败——两者都是有效的替代方案。

// 返回缓存数据或新鲜数据的 API
func fetchUser(id string) mo.Either[CachedUser, FreshUser] {
    if cached, ok := cache.Get(id); ok {
        return mo.Left[CachedUser, FreshUser](cached)
    }
    return mo.Right[CachedUser, FreshUser](db.Fetch(id))
}

// 模式匹配
result := fetchUser("user-123")
result.Match(
    func(cached CachedUser) mo.Either[CachedUser, FreshUser] { /* 使用缓存 */ },
    func(fresh FreshUser) mo.Either[CachedUser, FreshUser] { /* 使用新鲜数据 */ },
)

何时使用 Either 与 Result: 当一条路径是错误时使用 Result[T]。当两条路径都是有效的替代方案(缓存 vs 新鲜、左 vs 右、策略 A vs B)时使用 Either[L, R]

Either3[T1, T2, T3]Either4Either5 将其扩展到 3-5 种类型变体。

完整 API 参考,请参见 Either 参考

Do 表示法——具有单子安全性的命令式风格

mo.Do 将命令式代码包装在 Result 中,捕获来自 MustGet() 调用的 panic:

result := mo.Do(func() int {
    // MustGet 在 None/Err 时 panic——Do 将其捕获为 Result 错误
    a := mo.Some(21).MustGet()
    b := mo.Ok(2).MustGet()
    return a * b  // 42
})
// result 是 Ok(42)

result := mo.Do(func() int {
    val := mo.None[int]().MustGet()  // panic
    return val
})
// result 是 Err("no such element")

Do 表示法桥接了命令式 Go 风格与单子安全性——编写直线代码,自动传播错误。

管道子包 vs 直接链式调用

samber/mo 提供了两种组合操作的方式:

直接方法.Map.FlatMap)——当输出类型等于输入类型时有效:

opt := mo.Some(42)
doubled := opt.Map(func(v int) (int, bool) {
    return v * 2, true
})  // Option[int]

子包函数option.Mapresult.Map)——当输出类型与输入不同时需要:

import "github.com/samber/mo/option"

// int -> string 类型变化:使用子包 Map
strOpt := option.Map(func(v int) string {
    return fmt.Sprintf("value: %d", v)
})(mo.Some(42))  // Option[string]

Pipe 函数option.Pipe3result.Pipe3)——可读地链式组合多个类型转换:

import "github.com/samber/mo/option"

result := option.Pipe3(
    mo.Some(42),
    option.Map(func(v int) string { return strconv.Itoa(v) }),
    option.Map(func(s string) []byte { return []byte(s) }),
    option.FlatMap(func(b []byte) mo.Option[string] {
        if len(b) > 0 { return mo.Some(string(b)) }
        return mo.None[string]()
    }),
)

经验法则: 同类型转换使用直接方法。当步骤间类型变化时使用子包函数 + pipes。

详细的管道 API 参考,请参见 管道参考

常见模式

使用 Option 的 JSON API 响应

type UserResponse struct {
    Name     string            `json:"name"`
    Nickname mo.Option[string] `json:"nickname"`  // 优雅地省略 null
    Bio      mo.Option[string] `json:"bio"`
}

数据库可空列

type User struct {
    ID       int
    Email    string
    Phone    mo.Option[string]  // 实现 sql.Scanner + driver.Valuer
}

err := row.Scan(&u.ID, &u.Email, &u.Phone)

包装现有 Go API

// 将 map 查找转换为 Option
func MapGet[K comparable, V any](m map[K]V, key K) mo.Option[V] {
    return mo.TupleToOption(m[key])  // m[key] 返回 (V, bool)
}

使用 Fold 统一提取

mo.Fold 通过 Foldable 接口统一适用于 Option、Result 和 Either:

str := mo.Fold[error, int, string](
    mo.Ok(42),  // 适用于 Option、Result 或 Either
    func(v int) string { return fmt.Sprintf("got %d", v) },
    func(err error) string { return "failed" },
)
// "got 42"

最佳实践

  1. 优先使用 OrElse 而非 MustGet——MustGet 在值缺失/错误时 panic;仅在 mo.Do 块内(panic 会被捕获)或确定值存在时使用
  2. 在 API 边界使用 TupleToResult——在边界将 Go 的 (T, error) 转换为 Result[T],然后在领域逻辑内部使用 Map/FlatMap 链式调用
  3. 错误使用 Result[T],替代方案使用 Either[L, R]——Result 专用于成功/失败;Either 用于两种有效类型
  4. Option 用于可空字段,而非零值——Option[string] 区分“缺失”和“空字符串”;当空字符串是有效值时使用普通 string
  5. 链式调用,不要嵌套——result.Map(...).FlatMap(...).OrElse(default) 从左到右阅读;当单子链式调用更清晰时避免嵌套的 if/else 模式
  6. 多步骤类型转换使用子包 pipes——当 3 步以上且每一步都改变类型时,option.Pipe3(...) 比嵌套函数调用更可读

有关高级类型(Future、IO、Task、State),请参见 高级类型参考

如果你在 samber/mo 中遇到错误或意外行为,请在 https://github.com/samber/mo/issues 提交 issue。

交叉引用

  • -> 参见 samber/cc-skills-golang@golang-samber-lo 技能,了解可与 mo 类型组合的函数式集合转换(Map、Filter、Reduce on slices)
  • -> 参见 samber/cc-skills-golang@golang-error-handling 技能,了解惯用的 Go 错误处理模式
  • -> 参见 samber/cc-skills-golang@golang-safety 技能,了解 nil 安全和防御性 Go 编码
  • -> 参见 samber/cc-skills-golang@golang-database 技能,了解数据库访问模式
  • -> 参见 samber/cc-skills-golang@golang-design-patterns 技能,了解函数式选项和其他 Go 模式