2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > GCTT 出品 | Go:切片陷阱

GCTT 出品 | Go:切片陷阱

时间:2019-03-30 05:28:44

相关推荐

GCTT 出品 | Go:切片陷阱

前言

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

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

基本用法

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

你可以这样声明一个 slice:

var a []int

带字面量的 slice:

a := []int{1, 2}

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

数组 :

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

Slices:

// 全 0 slice, 大小为 4a := make([]int, 4)// 有字面量的全 0 slice, 大小为 3b := []int{2: 0}// 有字面量的全 0 slice, 大小为 2c := []int{0, 0}// 以下是允许的 :[]int 和 []int 是相同的类型a = bc = 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// 底层数组 datadata *[...]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 开始,产生了不一样的结果,因为当我们拿到 slicec时,它已经增长到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。

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

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

作者:Rob

译者:LSivan

校对:polaris1119

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

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。