前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Robust generic functions on slices

Robust generic functions on slices

作者头像
孟斯特
发布2024-02-29 15:41:50
770
发布2024-02-29 15:41:50
举报
文章被收录于专栏:code人生

原文在这里[1]。

由 Valentin Deleplace发布于2024年2月22日

slices[2]包提供了适用于任何类型切片的函数。在这篇博文中,我们将通过理解切片在内存中的表示方式的讨论以及它对垃圾收集器的影响,来更有效地使用这些函数,此外,我们还将介绍最近对这些函数进行的调整,使它们更加符合预期。

使用类型参数[3],我们可以为所有可比较元素的切片编写类似slices.Index[4]的函数,而不是为每种不同类型的元素都重新实现一遍:

代码语言:javascript
复制
// Index returns the index of the first occurrence of v in s,
// or -1 if not present.
func Index[S ~[]E, E comparable](s S, v E) int {
    for i := range s {
        if v == s[i] {
            return i
        }
    }
    return -1
}

slices[5]包包含许多这样的辅助函数,用于在切片上执行常见操作:

代码语言:javascript
复制
s := []string{"Bat", "Fox", "Owl", "Fox"}
s2 := slices.Clone(s)
slices.Sort(s2)
fmt.Println(s2) // [Bat Fox Fox Owl]
s2 = slices.Compact(s2)
fmt.Println(s2)                  // [Bat Fox Owl]
fmt.Println(slices.Equal(s, s2)) // false

一些新的函数(如InsertReplaceDelete等)会修改切片。为了理解它们的工作原理以及如何正确使用它们,我们需要了解切片的底层结构。

切片是对数组的一部分的视图。在底层[6],切片包含一个指针、一个长度和一个容量。两个切片可以有相同的底层数组,并且可以查看重叠的部分。

例如,这个切片s是对一个大小为6的数组的4个元素的视图:

如果一个函数改变了作为参数传递的切片的长度,那么它需要向调用者返回一个新的切片。如果底层数组不需要增长,那么它可能仍然保持相同。这解释了为什么append[7]和slices.Compact返回一个值,但是仅重新排序元素的slices.Sort不返回值。

要删除切片s中的一部分元素。在泛型之前,从切片s中删除部分s[2:5]的标准方式是调用append[8]函数将结束部分复制到中间部分:

代码语言:javascript
复制
s = append(s[:2], s[5:]...)

这种语法复杂且容易出错,因为涉及到子切片和可变参数。现在我们添加了slice.Delete[9]来更轻松地删除元素:

代码语言:javascript
复制
func Delete[S ~[]E, E any](s S, i, j int) S {
    return append(s[:i], s[j:]...)
}

这一行的Delete函数更清晰地表达了程序员的意图。现在我们假设有一个长度为6、容量为8的切片s,其中包含指针:

现在从切片s中删除s[2]s[3]s[4]

代码语言:javascript
复制
s = slices.Delete(s, 2, 5)

在索引2、3、4处的空白是通过将元素s[5]向左移动来填充的,并将新长度设置为3。

Delete不需要分配新的数组,因为它在原地移动元素。与append类似,它返回一个新的切片。在slices包中,许多其他函数都遵循这个模式,包括CompactCompactFuncDeleteFuncGrowInsertReplace

调用这些函数时,我们必须明确的是原始切片已经无效了,因为底层数组已经被修改。忽略返回值调用这些函数将是一个错误:

代码语言:javascript
复制
slices.Delete(s, 2, 5) // incorrect!
// s still has the same length, but modified contents

不需要的存活性问题

在Go 1.22之前,slices.Delete不会修改切片新长度和原始长度之间的元素。虽然返回的切片不会包含这些元素,但是在原始切片末尾创建的“间隙”仍然保留了它们。这些元素可能包含对大对象(例如 20MB 的图像)的指针,垃圾回收器不会释放与这些对象相关联的内存。这导致了可能引起显著性能问题的内存泄漏。

在上面的示例中,我们成功地从s[2:5]中删除了指针p2p3p4,通过将一个元素左移。但是p3p4仍然存在于底层数组中,超出了s的新长度。垃圾回收器不会回收它们。不太明显的是,p5不是被删除的元素之一,但由于p5指针保留在数组的灰色部分中,其内存可能仍然泄漏。

如果开发人员不知道“不可见”元素仍在使用内存,可能会导致混淆。

因此,我们有两个选择:

•保留Delete的高效实现。如果用户希望确保指向的值可以被释放,让他们自己将过时的指针设置为nil。•或更改Delete,始终将过时的元素设置为零。这将带来额外的工作,使Delete稍微不那么高效。将指针清零(将它们设置为nil)可以使这些对象在无法访问时启用垃圾回收。

哪一个更好呢?第一个提供了默认的性能,而第二个提供了默认的内存节约。

修复方法

“将废弃的指针设置为nil”并不像看起来那么容易。事实上,这个任务非常容易出错,我们不应该让用户自己来完成。出于实用主义的考虑,“清除尾部”,我们选择修改CompactCompactFuncDeleteDeleteFuncReplace 这五个函数的实现。其结果就是,认知负担减轻了,用户现在无需担心这些内存泄漏。

在Go 1.22中,调用Delete后内存的情况如下:

体现在代码中,就是这五个函数中使用了新的内置函数clear[10](Go 1.21),将废弃的元素设置s的元素类型的零值:

E是指针、切片、映射、通道或接口类型时,E的零值是nil

测试验证

当切片函数被错误使用时,这一更改导致了一些在Go 1.21中通过的测试在Go 1.22中失败。这是个好消息。当你有一个 bug 时,测试应该能够提醒你。

如果忽略Delete的返回值:

代码语言:javascript
复制
slices.Delete(s, 2, 3)  // !! INCORRECT !!

那么你可能会错误地假设s不包含任何nil指针,可以在Go Playground[11]中查看具体示例。

如果你忽略 Compact 的返回值:

代码语言:javascript
复制
slices.Sort(s)       // 正确
slices.Compact(s)    // !! 不正确 !!

那么你可能错误地假设s已经正确排序和压缩。示例[12]。

如果你将Delete的返回值赋给另一个变量,并继续使用原始切片:

代码语言:javascript
复制
u := slices.Delete(s, 2, 3)  // !! 不正确,如果继续使用 s !!

那么你可能错误地假设s不包含任何nil指针。示例[13]。

如果你意外地遮蔽了切片变量,并继续使用原始切片:

代码语言:javascript
复制
s := slices.Delete(s, 2, 3)  // !! 不正确,使用 := 而不是 = !!

那么你可能错误地假设s不包含任何nil指针。示例[14]。

最后

slices包的API比传统的泛型前语法在删除或插入元素方面有了很大的改善。

我们鼓励开发者使用新的函数,同时避免上面列出的一些“陷阱”。

得益于最近的实现更改,在没有任何API更改且开发人员无需进行额外工作的情况下,可以自动避免一类内存泄漏。

扩展阅读

slices包中函数的签名深受内存中表示切片的具体细节的影响。我们建议您阅读以下文档:

•「Go Slices: usage and internals](https://go.dev/blog/slices-intro)(Go Slices:用法和内部机制)•「Arrays, slices: The mechanics of 'append'](https://go.dev/blog/slices)(数组、切片:“append”的机制)•The dynamic array[15] data structure(动态数组数据结构)•slices包的文档[16]

关于将废弃元素清零的原始提案[17]包含许多细节和注释。

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)[18]进行许可,使用时请注明出处。 Author: mengbin[19] blog: mengbin[20] Github: mengbin92[21] cnblogs: 恋水无意[22] 腾讯云开发者社区:孟斯特[23]

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-02-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 孟斯特 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 不需要的存活性问题
  • 修复方法
  • 测试验证
  • 最后
  • 扩展阅读
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档