专栏名称: GoCN
最具规模和生命力的 Go 开发者社区
目录
相关文章推荐
湖南日报  ·  拟录用本科生惹争议,删除公示名单?国企回应 ·  15 小时前  
湖南日报  ·  邓正安被查 ·  19 小时前  
湖南日报  ·  特斯拉,突发! ·  昨天  
湖南日报  ·  一本族谱+一杯芝麻豆子茶+?=“靶材之王” ·  2 天前  
湖南建设投资集团有限责任公司  ·  风采展示 | ... ·  2 天前  
51好读  ›  专栏  ›  GoCN

Go 1.22 的新增功能系列之三:slices.Concat

GoCN  · 公众号  ·  · 2024-05-06 19:16

正文

我们正在准备 Go 1.22 的第二个候选版本,它应该很快就会发布。在我的上一篇博文中,我写了关于 Go 1.22 的 reflect.TypeFor 的工作。这次,我将写一下我是如何提出和实现 slices.Concat 的。

这是 slices.Concat 的签名:

// Concat returns a new slice concatenating the passed in slices.func Concat[S ~[]E, E any](slices ...S) S

这是通用切片库中一个非常简单的函数,事实上,我早在 2021 年 5 月就提出了它。在随后讨论要添加到切片包中的内容时,遭到了拒绝,

已解决:决定不在初始集中添加 Concat。如果有重要证据,我们总是可以重新考虑。

就上下文而言,slices 包本身直到 Go 1.21 才添加到 Go 标准库中。在此之前,它仅以预览形式作为外部 golang.org/x/exp/slices 包提供。slices 包故意保持小而集中,重点是在将其永久添加到标准库之前获得现实世界的经验。不过,最终我为 slices.Concat 写了一个新提案,并得到了提案审核组的批准,所以我也实现了它。

让我对当前 slices 包感到紧张的一件事是,通过在循环中滥用 O(N) 函数来制作意外二次的软件太容易了。在添加 slices.DeleteFunc 之前,我快速浏览了 SourceGraph 上列出的 Go 代码,发现大约四分之一的 slices.Delete 的使用都是在循环中完成的,这可能会导致性能二次下降,因为切片一遍又一遍地扫描和复印。

这种性能问题很隐蔽,因为对于小切片,性能影响可能可以忽略不计,但仅大十倍的切片可能需要花费一百倍的时间来处理,这导致程序在测试中运行良好,但在生产中却卡住了。我喜欢 slices.Concat 的一点是,代码阅读器对性能的影响应该相对清楚。想要在循环中使用 append slices.Insert 将一堆切片连接在一起的程序员将有望注意到 slices.Concat 的存在并使用它。

slices.Concat 的实现中有一些奇怪的地方,如果您还不知道它为什么在那里,可能需要解释一下。为了有效地将切片连接在一起, slices.Concat 会对一个新切片进行一次分配,其长度足以容纳所有连接的部分,而不是随着切片的进行而追加,并冒着在构建新切片时进行多次分配的风险。但是获取所有部分长度的代码中有一个看似多余的检查:

size := 0for _, s := range slices {  size += len(s)  if size < 0 {    panic("len out of range")  }}

在每个循环上检查大小是否小于零的原因是为了测试切片长度是否溢出。如果连接切片中的项目数量太大,它可能会回绕并变为负数。

溢出错误可能相当隐蔽。早在 2006 年,Josh Bloch 就写了一篇有关二分搜索常见实现中的溢出错误的文章,该错误在大约二十年里没有被注意到:

对于长度(以元素为单位)为 2 30 或更大(大约十亿个元素)的数组,此错误可能会显现出来。这在 80 年代编写《Programming Pearls》时是不可想象的,但现在在 Google 和其他地方很常见。Bentley 在《Programming Pearls》中表示,“虽然第一个二分搜索于 1946 年发布,但第一个对所有 n 值都正确工作的二分搜索直到 1962 年才出现。”事实是,很少有正确的版本发布过,至少在主流编程语言中是如此。

希望 slices.Concat 永远不会遇到这个特定的溢出问题,但测试此代码提出了一个有趣的挑战。如何在不使用 EB 级 RAM 进行测试的情况下,在 64 位机器上创建一个接近溢出所需长度的切片?答案是使用 struct{} 的切片,它除了切片头之外不占用额外的内存。

值得注意的是,如果您只想连接两个切片,则无需使用 slices.Concat 。您可以将内置附加函数与 ... 扩展运算符结合使用:

concat := append(s1, s2...)

Concat 提案的早期版本包含目标切片参数,例如 append 。( Concat(dest []T, ss ...[]T) []T 。)为什么 slices.Concat 的最终版本没有目标参数来允许用户重用现有切片作为支持?

这个问题又回到了所谓的混叠问题。如果您连接到一个 nil 切片或某个容量不足以容纳所有连接片段的切片,那么不会有任何问题,因为必须创建一个新切片,并将所有部分复制到其上。但是,如果您将切片的各个部分连接到其自身上该怎么办?举个例子:

s := []int{1, 2, 3, 4}_ = slices.ConcatWithDestination(s[:0], s[3:4], s[2:3], s[1:2], s[0:1])// What is s now?

slices.ConcatWithDestination 的简单实现会用 4 破坏切片开头的 1,然后将其复制到切片的末尾,这样最终得到的是 4, 3, 3, 4 而不是 4, 3、2、1 按预期。
碰巧,slices.Insert 和 slices.Replace 也有这个问题。修复效果不太好。该代码最终使用 unsafe 包来检查切片是否重叠。如果这样做,它会通过将值轮换到位来避免分配:







请到「今天看啥」查看全文


推荐文章
湖南日报  ·  邓正安被查
19 小时前
湖南日报  ·  特斯拉,突发!
昨天
湖南建设投资集团有限责任公司  ·  风采展示 | 集团劳动模范:王群英、谭勇、熊先勇、刘军
2 天前
金融先生MrFinance  ·  不要把你宝贵的时间,浪费在兼职上
8 年前
金融先生MrFinance  ·  不要把你宝贵的时间,浪费在兼职上
8 年前
庞门正道  ·  不用钱的道具 也能拍出好照片
8 年前