Go语言slice详解

slice 的本质

slice 本质上是基于数组实现的,slice 可以看作是由三个元素组成的结构体:

struct slice {
  ptr
  len
  cap
}

其中 ptr 是指向底层数组的指针, len 表示当前 slice 中元素的数量, cap 表示当前底层数组大小.

我们用 make 来创建 slice 的时候, 最多可以指定三个参数:

make([]Type, len, cap)

其中第三个参数是可选的, 用于指定底层数组的大小, 如果未指定, 则默认是和第二个参数是一致的.

用 make 创建的指定大小的数组会用类型的 0 值进行初始化, 例如对于下面的代码:

s := make([]int, 2)
s = append(s, 2, 3)
fmt.Println(s)

// Result:
// [0 0 2 3]

它的输出结果是 [0 0 2 3], 这是因为用 make 创建 slice 的时候, 里面已经存在了 2 个 0 值元素.

这里需要指出的一点是对 slice 的赋值操作是 O(1) 的, 它和底层数组的大小没有关系, 因为我们只需要把 (ptr, len, cap) 这三个值拷贝到新的 slice 即可.

子 slice

我们可以通过 slice[begin:end:cap_idx] 来获取一个子 slice, 子 slice 的大小是 end - begin, 其中 end 和 cap_idx 最大可以设置为 cap(slice). 子 slice 相当于是:

  • ptr = slice.ptr + begin
  • len = end - begin
  • cap = cap_idx - begin 

当我们往 slice 中 append 数据时, 如果 slice 还有容量时, 直接 slice[len] = newValue 即可:

s := make([]int, 0, 2)
_ = append(s, 1, 2)
s1 := s[0:2:2]
fmt.Println(s1)

// Result:
// [1 2]

但如果 append 的数据超过当前 slice 的容量时, 便会重新申请一个数组存放要添加的数据. 例如我们往上面例子中的 slice 再添加一个新的数据时, 便会超过之前的容量而去重新申请一个数组. 这样之前数组里的内容便会还是默认值, 输出结果为: [0 0]

s := make([]int, 0, 2)
_ = append(s, 1, 2, 3)
s1 := s[0:2:2]

fmt.Println(s1)

// Result:
// [0 0]

往slice里面添加数据

a := make([]int, 0, 10)
b := append(a, 1)

往slice a里面添加一个1的方式,就是上面的代码。不过实际使用中,我们常用的是这种方式:

a = append(a, 1)

那现在考虑这一行代码:

b := append(a, 1)

b和a的关系:如果len(a)+1<=cap(a),这个时候a内部的数组仍足够存储新添加的数据,此时,b的ptr和a的ptr是相同的。此时:b.ptr == a.ptr, len(b) == len(a)+1, cap(b) == cap(a)。

对一个slice重新切片

这里重新切片的意思,就是取slice里面的一部分元素。

a := make([]int, 10, 20)
b := a[0:5]

如上面这段代码,就是重新切片的。此时b.ptr == a.ptr。b.cap == a.cap。b.len == 5。

 

好了,了解了上面的基础属性,现在就可以开始练练手了。

1. 看看下面的代码会输出什么:

package main

func main() {
	a := make([]int, 0)
	b := append(a, 1)
	_ = append(a, 2)
	println(b[0])
}

我们往a里面添加了一个1成为了b。这个时候输出的是1,好像没什么问题。那下面这段代码会输出什么呢?

package main
func main() {
	a := make([]int, 0, 10)
	b := append(a, 1)
	_ = append(a, 2)
	println(b[0])
}

嗯,是2。这个我觉得就是使用slice的时候最大的坑。但理解了它们内部的存储方式,也就不难理解为什么是这样子了。

执行完:

b := append(a, 1)

此时b[0]的确是1。但此时b.ptr == a.ptr。因为这个时候cap(a)为10,足以存储新插入的元素1。

执行:

_ = append(a, 2)

此时,cap(a)仍然为10,len(a)仍然为0,往a里面插入元素2 ,使得ptr[0]==2。由于b.ptr与a.ptr相同,b里面的数据就被改掉了。

 

2. 看看下面的代码会输出什么:

package main
func main() {
	a := make([]int, 10, 20)
	b := a[5:]
	println(len(b), cap(b))
}
答案是:5 15

输出5是因为a的长度为10,b := a[5:],相当于是对a重新切片,取a第5个元素以后的值。a第5个元素之后还剩下5个值,那len(b)就是5了。

cap(b)为什么为15呢?

因为此时, b.ptr = a.ptr + 5。也就是b内部指针,指向了a.ptr的后面第5个元素。所以此时cap(b)就不能是20了,因为b无法利用a前面的5个元素。

3. 如果避免重新切片之后的新切片,不被修改?如下所示:

package main

import (
	"fmt"
)

func doAppend(a []int) {
    _ = append(a, 0)
}

func main() {
	a := []int{1, 2, 3, 4, 5}
	doAppend(a[0:2])
	fmt.Println(a)
}

这段代码会输出:

[1 2 0 4 5]

虽然我们调用doAppend的时候,只把2个元素传入了。但它却把a的第3个元素改掉了。如何避免呢?答案如下:

package main

import (
	"fmt"
)


func doAppend(a []int) {
	_ = append(a, 0)
}


func main() {
	a := []int{1, 2, 3, 4, 5}
	doAppend(a[0:2:2])
	fmt.Println(a)
}

就是在对slice重新切片的时候,加入第三个capacity参数。

doAppend(a[0:2:2])

最后的2,就是指定了重新切片之后新的slice的capacity。我们指定它的capacity就是2,所以,doAppend函数进行append操作的时候,发现capacity不够3,就会重新分配内存。这时就不会修改原有slice的内容了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值