在 Go1.18 中实现一个简单的 Result 类型
争做团队核心程序员,关注「幽鬼」
大家好,我是程序员幽鬼。
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)
}
不过,我想指出一些明显的事情。
Golang 还没有 do sum 类型,有人建议添加,但我认为它会在很久以后出现。目前,在 Golang 中模拟 sum 类型的最佳方法是简单地使用一个结构体并记住你使用了哪个字段,就像我们在这里对 Result 类型所做的那样。如果你包含一个表示正在使用的字段的特殊字段,其他语言将此称为可区分联合(discrimated union)。 Result
类型,无论你用什么方法来实现它,都不是一个新概念。从 C++17 开始有std::optional
。Rust
有Result<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
往期推荐
欢迎关注「幽鬼」,像她一样做团队的核心。