在 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)
}

不過,我想指出一些明顯的事情。

  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

歡迎關注「幽鬼」,像她一樣做團隊的核心。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MAKcuI6M8xcbP3bSkkimoQ