Go 1.22 的新增功能系列之一:cmp.Or
共 3592字,需浏览 8分钟
·
2024-04-25 17:15
截至撰写本文时,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) 的故事。