Go 1-22 的新增功能系列之一:cmp-Or

截至撰寫本文時,Go 1.22 已經發布幾個月了。早就該結束我爲 1.22 所做的工作的系列了。抱歉耽擱了這麼久,我最近忙於生活事務。如果您錯過了我關於 reflect.TypeFor(https://blog.carlana.net/post/2024/golang-reflect-type-for/) 和 slices.Concat(https://blog.carlana.net/post/2024/golang-slices-concat/) 的帖子,請務必關注這些帖子。

我爲 Go 1.22 提出並實現的最終函數是 cmp.Or。在 Go Time 中,我將其稱爲 “1.22 的隱藏寶石”。這是一個簡單的函數,但有很多潛在的用途和令人驚訝的長背景故事。

首先我們看一下代碼:

// Or returns the first of its arguments that is not equal to the zero value.
// If no argument is non-zero, it returns the zero value.
func Or[T comparable](vals ...T) T {
  var zero T
  for _, val := range vals {
    if val != zero {
      return val
    }
  }
  return zero
}

正如我對 Go 的貢獻一樣,它非常簡短。它只是比較其參數並返回第一個不是 0 或 nil 或 "" 或其類型的零值的參數。

你如何使用它?

cmp.Or 的主要用途是獲取字符串並返回第一個非空白字符串。例如,在搜索公共開源 Go 存儲庫時,我在網上發現了很多代碼,這些代碼嘗試獲取環境變量,但如果環境變量爲空則返回默認值。對於 cmp.Or ,這看起來像 cmp.Or(os.Getenv("SOME_VARIABLE"), "default") 。

它也適用於數字和指針。

以下是我的真實代碼庫中的一些實際用途:

body := cmp.Or(page.Body, rawContent)
name := cmp.Or(jwt.Username(), "Almanack")
credits = append(credits, cmp.Or(credit.Name, credit.Byline))
metadata.InternalID = cmp.Or(
    xhtml.InnerText(rows.Value("slug")),
    xhtml.InnerText(rows.Value("internal id")),
    metadata.InternalID,
)
scope.SetTag("username", cmp.Or(userinfo.Username(), "anonymous"))
currentUl = cmp.Or(
    xhtml.Closest(currentUl.Parent, xhtml.WithAtom(atom.Ul)),
    currentUl,
)

正如您所看到的,大多數用途只是查看字符串來提供後備值,但最後一個示例是尋找非零的 *html.Node 。

cmp.Or 的另一個主要用途是與 cmp.Compare 一起使用來創建多部分比較:

type Order struct {
  Product  string
  Customer string
  Price    float64
}
orders := []Order{
  {"foo", "alice", 1.00},
  {"bar", "bob", 3.00},
  {"baz", "carol", 4.00},
  {"foo", "alice", 2.00},
  {"bar", "carol", 1.00},
  {"foo", "bob", 4.00},
}
// Sort by customer first, product second, and last by higher price
slices.SortFunc(orders, func(a, b Order) int {
  return cmp.Or(
    cmp.Compare(a.Customer, b.Customer),
    cmp.Compare(a.Product, b.Product),
    cmp.Compare(b.Price, a.Price),
  )
})
foo alice 2.00
foo alice 1.00
bar bob 3.00
foo bob 4.00
bar carol 1.00
baz carol 4.00

請注意,由於 cmp.Or 無法進行短路評估,因此即使客戶名稱不同,也會比較每個商品的產品名稱和價格,這使得這樣做是多餘的。

通往 cmp.Or 的路很長。早在 2016 年,Stephen Kampmann 就提出了 strings.First,但那是在現代 Go 提案系統之前,因此該提案實際上並未得到評估。2020 年,我提出了一個新的運算符 ?? ,其工作方式與 cmp.Or 類似,但具有短路功能。它可以像 port := os.Getenv("PORT") ?? DefaultPort 一樣使用。Ian Lance Taylor 當時指出,如果 Go 擁有泛型,則可以將其實現(無需短路)作爲泛型輔助函數。在打開 ?? 的問題後不久,我最終編寫了一個 stringutils.First 輔助函數供我個人使用,並且我很快發現自己在代碼中到處使用它。

當泛型最終在 2021 年添加到 Go 的 beta 版本中時,我編寫了一個名爲 truthy 的包,它使用 reflect.Value.IsZero() 來報告任何值是否爲零,但我發現使用反射會導致分配(這最終得到了改進)比正常比較慢 50 倍(在 Go 1.22 中已改進爲僅 25 倍)。在當前版本的 Go 中,僅使用帶有 comparable 的泛型(如 cmp.Or )而不使用反射會造成大約 2 倍的損失。

2022 年,我提出了一項關於通用通用零值的提案,該提案作爲現有討論的重複而結束,但 Russ Cox 在 2023 年提出了基本相同的提案,並被接受。根據提案,內置常量 zero 將可在泛型函數中用於返回或比較。然而,該提案被接受後,由於社區持續反對在該語言中同時使用 nil 和 zero 的想法,該提案被撤回。我仍然認爲這是一種恥辱,並希望有一天 cmp.Or 可以完全通用並適用於任何類型,但目前,它僅適用於 comparable 類型。

最初,我建議只將 strings.First 添加到標準庫中,但在討論過程中,一些評論者更喜歡該函數的通用版本,Russ Cox 提出了名稱 cmp.Or ,最終被接受。

我在 Go 1.22 上的工作就到此結束了!今年夏天請回來瞭解 reflect.Value.Seq()(https://github.com/golang/go/issues/66056) 的故事。

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