Go 1.18 泛型的一些技巧和困擾

Cluas發表於2021-11-18

截至2021年11月17日,社群可能還沒有使用 Go 1.18 泛型功能的快取庫。

我嘗試在這裡實現了第一個 Go 1.18 泛型的快取庫。如果你能夠給的 GitHub 加個 Star,我會感到非常高興。 https://github.com/Code-Hex/go-generics-cache

在這篇文章中,我將介紹我在開發這個快取庫時遇到的關於 Go 泛型的一些情況,以及我發現的一些技巧和困擾。

對任何型別都返回零值

你經常會寫一些返回 anyerror的程式碼,比如說下面這樣。當一個函式發生錯誤時,你會寫一些返回零值和錯誤的程式碼,但現在你需要換一種思維方式。

func Do[V any](v V) (V, error) {
    if err := validate(v); err != nil {
        // What should we return here?
    }
    return v, nil
}

func validate[V any](v V) error

假設你在這裡寫return 0, err。這將是一個編譯錯誤。原因是any型別可以是int型別以外的型別,比如string型別。那麼我們應該怎麼做呢?

讓我們用型別引數的V宣告一次變數。然後你可以把它寫成可編譯的形式,如下:

func Do[V any](v V) (V, error) {
    var ret V
    if err := validate(v); err != nil {
        return ret, err
    }
    return v, nil
}

此外,可以使用帶命名的返回值來簡化單行的書寫。

func Do[V any](v V) (ret V, _ error) {
    if err := validate(v); err != nil {
        return ret, err
    }
    return v, nil
}

https://gotipplay.golang.org/p/0UqA0PIO9X8

不要試圖用約束做型別轉換

我想提供兩個方法,IncrementDecrement。它們可以從go-generics-cache庫中增加或減少值,如果儲存的值滿足Number 約束

讓我們用Increment方法作為一個例子。我最初寫的程式碼是這樣的:

type Cache[K comparable, V any] struct {
    items map[K]V
}

func (c *Cache[K, V]) Increment(k K, n V) (val V, _ error) {
    got, ok := c.items[k]
    if !ok {
        return val, errors.New("not found")
    }

    switch (interface{})(n).(type) {
    case Number:
        nv := got + n
        c.items[k] = nv
        return nv, nil
    }
    return val, nil
}

我在考慮使用值n V的型別來匹配被滿足的約束。如果滿足Number約束,這個方法就會增加,否則什麼都不做。

這將不會被編譯。

  1. Go 不為約束條件提供條件分支
  2. 約束是一個介面,Go 不允許使用介面進行型別斷言
  3. n的型別沒有確定,所以+操作是不可能的
  4. 首先,不能保證items的型別與n的型別相同

為了解決這些問題,我決定嵌入Cache結構。我還定義了一個NumberCache結構,可以一直處理Number約束。

  • 繼承 Cache結構體所持有的欄位資料
  • 處理 Cache的方法
type NumberCache[K comparable, V Number] struct {
    *Cache[K, V]
}

這樣,我們可以保證傳遞給Cache結構的值的型別永遠是Number的約束。所以我們可以給NumberCache結構新增一個Increment方法。

func (c *NumberCache[K, V]) Increment(k K, n V) (val V, _ error) {
    got, ok := c.Cache.items[k]
    if !ok {
        return val, errors.New("not found")
    }
    nv := got + n
    c.Cache.items[k] = nv
    return val, nil
}

https://gotipplay.golang.org/p/poQeWw4UE_L

使我困擾的點

讓我們再看一下Cache結構的定義。

type Cache[K comparable, V any] struct {
    items map[K]V
}

Go 範型被定義為一種帶有約束的語言規範,這種約束被稱為 comparable。這允許只有型別可以使用 ==!=

我覺得這個約束條件讓我很困擾。讓我解釋一下困擾我的原因。

我定義了一個函式來比較兩個 comparable 的值。

func Equal[T comparable](v1, v2 T) bool {
    return v1 == v2
}

只允許 comparable 的型別,如果在編譯時將不可比較的型別傳遞給函式,就會導致錯誤。你可能認為這很有用。

然而,根據 Go 的規範,interface{}也滿足這個可比較的約束。

如果interface{}可以被滿足,下面的程式碼就可以被編譯了。

func main() {
    v1 := interface{}(func() {})
    v2 := interface{}(func() {})
    Equal(v1, v2)
}

這表明func()型別是一個不可比較的型別。但可以通過將其轉換為interface{}型別來轉換為可比較的型別。

interface{}型別只有在執行時才能知道它是否是一個可比較的型別。

如果這是一段複雜的程式碼,可能很難被注意到。

https://gotipplay.golang.org/p/tbKKuehbzUv

我相信我們需要另一個不接受interface{}的可比約束,以便在編譯時注意到。

這種約束可以由 Go 使用者來定義嗎?目前的答案是不能。

這是因為comparable約束包含 "可比較的結構體" 和 "可比較的陣列"。

這些約束目前不能由 Go 使用者定義。因此,我想把它們作為 Go 規範來提供。

我還為此建立了一個提案,如果你也認同這個說法,請在 GitHub issue 上給我?,我將不勝感激。 https://github.com/golang/go/issues/49587

文中提到的連結

更多原創文章乾貨分享,請關注公眾號
  • Go 1.18 泛型的一些技巧和困擾
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章