查看原文
其他

在 Go1.18 中实现一个简单的 Result 类型

程序员ug 幽鬼 2022-09-08

争做团队核心程序员,关注「幽鬼

大家好,我是程序员幽鬼。

Go 中的错误处理一直是争议最多的。Rust 是通过引入 Result 类型来解决此问题。

随着 Go 1.18 中泛型的引入。Go 可以模仿 Rust 实现一个简单的 Result 类型。

在日常工作中,很多时候需要使用 goroutines 来实现 “map-reduce” 风格的算法。像这样:

func processing() {
    works := []Work{
        {
            Name: "John",
            Age:  30,
        },
        {
            Name: "Jane",
            Age:  25,
        },
        ...
    }

    var wg sync.WaitGroup

    result := make(chan ProcessedWork, len(works))
    for _, work := range works {
        wg.Add(1)
        go func(work Work) {
            defer wg.Done()
            // do something
            newData, err := doSomething(work)
            result <- newData
        }(work)
    }

    wg.Wait()
    close(result)

    for r := range result {
        // combine results in some way
    }
    return ...
}

这可能看起来一切都很好,但有一个问题。如果其中一个子 goroutine 失败了,如果这一行实际上返回了错误怎么办?

newData, err := doSomething(work)

所以通常你有两种选择,一种是简单地引入一个单独的错误通道,其次是在本地引入一个新的 Result 类型并改变result这里的通道以接受 Result 类型。在这个例子中,我们可以这样做:

type Result struct {
    Data ProcessedWork
    Err  error
}
result := make(chan Result, len(works))

我通常选择第二种解决方案,但每次都必须定义这种类型是一种痛苦。有一些建议用 interface{} 来表示数据并在使用数据时进行类型断言,但通常我并不喜欢使用 interface{}

幸运的是,Go 1.18 中有了泛型,因此Result可以基于泛型定义我们的类型。

type Result[T any] struct {
 value T
 err   error
}

对于上面代码中的每个 processing 函数,使用这种类型比使用特殊的 Result 类型要好得多。

许多有用的方法可以添加到 Result 类型中,例如:

func (r Result[T]) Ok() bool {
 return r.err == nil
}

func (r Result[T]) ValueOr(v T) T {
 if r.Ok() {
  return r.value
 }
 return v
}

func (r Result[T]) ValueOrPanic() T {
 if r.Ok() {
  return r.value
 }
 panic(r.err)
}

不过,我想指出一些明显的事情。

  1. Golang 还没有 do sum 类型,有人建议添加,但我认为它会在很久以后出现。目前,在 Golang 中模拟 sum 类型的最佳方法是简单地使用一个结构体并记住你使用了哪个字段,就像我们在这里对 Result 类型所做的那样。如果你包含一个表示正在使用的字段的特殊字段,其他语言将此称为可区分联合(discrimated union)。
  2. Result类型,无论你用什么方法来实现它,都不是一个新概念。从 C++17 开始有 std::optionalRustResult<T, E>。在 Haskell 中,我第一次学习了使用 Maybe 类型来表示可能成功也可能不成功的操作结果。

对于第 2 点,另一件需要注意的事情是,Result 实际上是一个不可分割的实体,C++23最近为std::optional 添加了monadic 操作,并且早在这个 conecpt 流行之前,Hasekll 在它的 stdlib 中就有以下函数:

return :: a -> Maybe a
return x  = Just x

(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
(>>=) m g = case m of
                Nothing -> Nothing
                Just x  -> g x

这两个函数的好处,尤其是对于第二个函数(>>=),它允许你轻松地组合/链接多个可能会或可能不会产生结果的操作,而无需继续使用if来检查上次操作的结果是否为 Ok。我不打算列举例子,但如果你很好奇,你可以在这里[1]查看 Hasekll 示例。

但是 Golang 有点欠缺,此时,如果一切按计划进行,那么我们将无法在泛型类型的方法中拥有另一个独立类型变量。所以我们不能有这样的做法:

func (r Result[T]) Then(f func(T) Result[S]) Result[S] { // <-- S is not allowed, we can only use T
 if r.Ok() {
  return f(r.value)
 }
 return r
}

此限制非常严格地限制了 Result 类型的有用性。

我希望拥有的另一件事是 Go 泛型中的 C++ 风格的 “partial specialization”。现在 Result 的约束是any,但我确实想为用户提供这样的功能:

func (r Result[T]) Eq(v T) bool {
    if r.Ok() {
        return r.value == v
    }
    return false
}

但由于T不是comparable== 无法工作。如果 Go 可以提供一种方法来细化泛型类型的某些方法的约束,那么这将是一个不错的功能。例如:

// here we refined the T type from any to comparable by providing a more precise constraints in the method receiver type
// now only Result that holds a T that are in the constraint comparable will have this method enabled.
func (r Result[T comparable]) Eq(v T) bool {
    if r.Ok() {
        return r.value == v
    }

    return false
}

示例代码可以在这里[2]找到

原文链接:https://csgrinding.xyz/go-result-1/

参考资料

[1]

这里: https://en.wikibooks.org/wiki/Haskell/Understanding_monads/Maybe

[2]

这里: https://github.com/bobfang1992/go-result



往期推荐


欢迎关注「幽鬼」,像她一样做团队的核心。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存