Go:切片陷阱

LSivan · 2019-03-30 15:57:42 · 1206 次点击 · 预计阅读时间 4 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2019-03-30 15:57:42 的文章,其中的信息可能已经有所发展或是发生改变。

前言

我最喜欢 Go 的一个特性就是,毫无惊喜。某些程度上可以说有点无聊的感觉。这是编程语言的一个优秀的品质。这样的话,在编码的时候就可以专注于手头上的问题,而不是语言做了你不希望它做的事情

这篇文章有关 Go 的一个对新人来说最 "惊喜" 的特性 : slice。

基本用法

如果你了解如何使用 Go slice, 请跳到下一节。

你可以这样声明一个 slice:

var a []int

带字面量的 slice:

a := []int{1, 2}

slice 是可变长度的集合。不像数组,slice 可以按需进行增长和切分。

数组 :

// 全 0 数组,大小为 4
var a [4]int
// 有字面量的全 0 数组,大小为 3
b := [...]int{2: 0}
// 有字面量的全 0 数组,大小为 2
c := [...]int{0, 0}
// 以下都不合法,[4]int, [3]int 以及 [2]int 是不一样的类型
a = b
c = b

Slices:

// 全 0 slice, 大小为 4
a := make([]int, 4)
// 有字面量的全 0 slice, 大小为 3
b := []int{2: 0}
// 有字面量的全 0 slice, 大小为 2
c := []int{0, 0}
// 以下是允许的 :[]int 和 []int 是相同的类型
a = b
c = a

而且,slice 还可以进行子切分:

a := []int{0, 1, 2, 3, 4}
b := a[1:3] /* [1, 2]          */
c := a[3:]  /* [3, 4]          */
d := a[:2]  /* [0, 1]          */
e := a[:]   /* [0, 1, 2, 3, 4] */

以及增长:

a := []int{1, 2}
b := append(a, a...) /* [1, 2, 1, 2] */
a = append(a, 3, 4)  /* [1, 2, 3, 4] */

这通常让 slice 成为所有应用场景首选的数据结构 ( 译者注:应该是对于所有适用数组和 slice 的场景而言,slice 胜于数组 )。

那么,有什么问题呢 ?

slice 不是其他东西,而是一个携带三份信息的 struct

type slice struct {
    // 在 data 中使用到的空间大小
    len  int
    // data 的大小
    cap  int
    // 底层数组 data
    data *[...]Type
}

当从 slice 中拿到一个 slice,cap, lendata 都可能会变化,但底层数组既不会进行重新分配,也不会进行复制。

这个特性导致了一些怪异的行为。

迷之更新:第一部分

a := []int{1, 2}
b := a[:1]     /* [1]     */
b[0] = 42      /* [42]    */
fmt.Println(a) /* [42, 2] */

这类技巧基本在 Gophers 的意料之中,通常是因为语言的某些核心接口依赖于 slice 的底层数据通过引用传递的事实。 例如,io.Reader 具有与 io.Writer 相同的类型签名,对于新人来说可能相当令人惊讶:

type Reader interface {
    // Read 把数据写到 p 中
    Read(p []byte) (n int, err error)
}
type Writer interface {
    // Write 从 p 中读取数据
    Write(p []byte) (n int, err error)
}

迷之更新:第 2 部分

这部分看起来更具迷惑性

a := []int{1, 2, 3, 4}
b := a[:2] /* [1, 2] */
c := a[2:] /* [3, 4] */
b = append(b, 5)
fmt.Println(a) /* [1 2 5 4] */
fmt.Println(b) /* [1 2 5]   */
fmt.Println(c) /* [5 4]     */

当数据被追加到 b,底层数组有足够的容量来保存多两个元素,所以 append 不会重新分配,这意味着,数据追加到 b 之后会改变 c

迷之更新:第 3 部分

a := []int{0}     /* [0]          */
a = append(a, 0)  /* [0, 0]       */
b := a[:]         /* [0, 0]       */
a = append(a, 2)  /* [0, 0, 2]    */
b = append(b, 1)  /* [0, 0, 1]    */
fmt.Println(a[2]) /* 2 <- 对的   */

// 一样的代码,只是以一个稍大的 slice 开始
c := []int{0, 0}  /* [0, 0]       */
c = append(c, 0)  /* [0, 0, 0]    */
d := c[:]         /* [0, 0, 0]    */
c = append(c, 2)  /* [0, 0, 0, 2] */
d = append(d, 1)  /* [0, 0, 0, 1] */
fmt.Println(c[3]) /* 1 <- ??      */

这个奇怪的行为的原因是,当 slice 变得比某个确切的阈值要大时,Go 停止线性增长并开始分配一个大小翻倍的 slice。这取决于 slice 类型的大小

分析更多的细节 :

  • 第一个在 a 上的 append 复制前一个 0 到一个 cap==2 的 slice, 然后在 a[1] 上填一个 0
  • a 拿到了一个 slice, len(b) == cap(b) == 2
  • 第二个在 a 上的 append 复制前面的 0 到一个 cap==4 的 slice, 然后在 a[2] 上填上 2
  • 在这里,b 依然还是 cap == 2, 所以在 bappend, 分配了一个新的底层数组

同样的过程,以初始 cap 为 2 的 slice 开始,产生了不一样的结果,因为当我们拿到 slice c 时,它已经增长到 cap == 4

碎碎念:由于这种行为取决于底层类型的大小,因此 []struct{}{} 将始终通过追加的元素的确切数量增长。

我该怎么解决这个问题呢?

如果你传递一个从不追加的 slice, 那么这是安全的。只需要紧紧记住,每一个 ( 传递的 slice) 都共享相同内存区域的 "视图"。如果你调用的函数在返回后不保留对 slice 的引用,也同样适用。

相反地,如果你打算传递可能要追加数据的 slice, 然后你也打算对原 slice 进行扩容,你可能会希望考虑限制你所分享的数据的容量。

a := append([]int{}, 0, 1, 2, 3)
// 如果 `potentialSliceGrower` 保持着对 `a` 的引用,下方这种调用可能是危险的
potentialSliceGrower(a)
// 这个是安全的,取一个确定大小的 slice( 进行传递 )
// 追加则会引起复制
potentialSliceGrower(a[:4:4])

这种不常使用的 3-index 语法,从 a 中拿到了一个从下标 0 开始,到下标 4 结束,cap=4 的 slice。

请在真正有需要的时候再使用它,但在需要的时候别忘记这个方法。

想要了解更多?

这儿有一个关于 slice 内部机制的 Go 的官方博客, 在文末还有其他陷阱


via: https://blogtitle.github.io/go-slices-gotchas/

作者:Rob  译者:LSivan  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1206 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传