Go Slice与append原理

一. 引言

最早在go设计的初期,设计者们花了一年的时间对array类型的定义进行讨论,因为像其他语言一样,数组一般被设计为定长的、长度属于类型的一部分的用来描述线性地址空间的数据结构,但是这种定长类型对于使用者比较受局限,所以类似像C++这样的语言会出现vector这样的数据结构,来弥补数组在动态特征方面的不足。go语言的设计者不希望对array进行差别定义,这样会增加其他语言迁移过来的学习者的理解成本,同时也不希望放弃线性数据结构的动态化特性,所以引入了slice这一概念,这就是slice的诞生背景。

二. 大咖说

根据Rob Pike在官方blog中的文章《Arrays, slices (and strings): The mechanics of ‘append’》中对slice的介绍,可以窥见slice的定义,原文中包含这样一句

A slice is a data structure describing a contiguous section of an array stored separately from the slice variable itself. A slice is not an array. A slice describes a piece of an array.
slice是一种描述了一个独立array中一段连续空间的数据结构,slice不是array,slice描述了array的一部分

而在Russ Cox的文章《Go Data Structures》中,这样定义了slice:

A slice is a reference to a section of an array
slice是对array中一部分数据的引用

从这里可以得到几个有用信息

  • slice不是array,它独立于array存在
  • slice描述/引用了array的一部分
  • slice基于array,没有array也就没有slice

理解了slice的设计初衷与结构,那么理解他的使用就变得容易了,这里还是要仰慕一下Rob Pike,文章功底也是不错的。在文章中,Rob Pike循序渐进地介绍了slice的设计。

三. Slice原理与基本操作

1. slicing

在数组的基础上,我们可以通过slicing操作获得一个切片,我们的代码中很少见array,但是这个对数组的slicing操作跟对slice的slicing操作没什么区别。由于slicing操作并没有拷贝array的数据,所以实际上它只是创建了一个描述slicing结果的空间,这其中保存了生成的slice的长度len和起始地址pointer,姑且称为slice结构。

func main(){
	var array [8]int    // declare array
	s := array[0:4] // slicing operation,len: 4,pointer: &array[0]
}
2. slice as parameter

正是由于slice结构只是描述了一段数组,所以它只需要保存开始地址和长度就可以了,那么在函数传参时,一个传递slice参数比较高效的方法,就是只传递slice结构,而不传递被slice描述的array空间本身。这保障了slice在传参时的效率。

func Foo(s1 []int) []int {
	s1 = s1[0 : len(s1)-1] // cut one element
	return s1
}

func main() {
	// ...
	s := []int{1,2,3,4}
	s2 := Foo(s)
	fmt.Println(len(s), len(s2))// print 4 3
}
3. slice as receiver

传值解决了效率问题,但函数内却无法将对slice结构的修改反馈到实参上。比较c/cpp style的解决方式就是将slice取地址传入。但是GO的设计者觉得这样并不优雅(“it seems clumsy”)。所以就产生了receiver这种GO style的方式。

type MySlice []int

// cut one element method
func (s *MySlice) Bar() {
	*s = (*s)[0 : len(*s)-1]
}

func main() {
	s := MySlice([]int{1, 2, 3, 4})
	fmt.Println(len(s)) // print 4
	s.Bar()
	fmt.Println(len(s)) // print 3
}
4. capacity of slice

如果slice只能做这些事情,那么它几乎就是另一个数组,slice和array就变成了文字游戏。于是slice就有了第三个描述字段 - cap。cap描述了slice的容量,与array不同的是,array的容量都写在脸上(类型的一部分),它的len和cap永远相等,而slice却不一定,len可以小于等于cap。这样一个微小的区别,让slice具备了一个array不具备的能力:它变得可扩展了。

func main() {
	var a [8]int
	s := a[1:4]
	fmt.Println(len(a), cap(a)) // print 8 8
	fmt.Println(len(s), cap(s)) // print 3 7
	s = s[0:4]                  // extend one element
	fmt.Println(len(s), cap(s)) // print 4 7
}
5. make & copy

前面看到的slice,是通过slicing操作获得的,他们的容量,是从slice描述的起始位置,到底层array的结束位置,slicing操作使我们没有时机调整slice的容量。为了可以显式创建一个指定长度和容量的slice,go提供了内置函数make,语法层面就参阅语言说明书吧

除此之外,数据的拷贝也是一个基础功能,内置copy函数提供了这样的功能,下面罗列一些copy函数的特性:

  • copy的长度是src或dst中len(不是cap)最少的那个(min(len(src), len(dst)))
  • copy的返回值是实际拷贝的元素数量
  • copy支持原地拷贝,也就是说,它避免了原址拷贝过程中的元素覆盖问题
6. appending to slice

这可能是日常用的最多的功能了,Rob Pike铺垫了很多,就是为了说明append操作的原理。append底层逻辑其实蛮简单的,它会修改slice的len,偶尔还会修改cap和pointer。
修改len很好理解,追加了元素的slice自然会变长,append方法还提供可变参数,允许向一个slice上append另一个slice,可以视为批量append。
当且仅当待append的元素数量 + 当前长度 > cap时,slice需要扩容和数据拷贝,这种时候cap和pointer就会被修改。
虽然描述起来很简单,但是不留神就会有很多坑,下面举个例子

func main() {
	a := [...]int{1, 2, 3, 4}   // explicit initialize of array
	s := a[1:3]                 // s = {2,3}
	fmt.Println(len(a), cap(a)) // print 4 4
	fmt.Println(len(s), cap(s)) // print 2 3
	s = append(s, 5)            // a = {1,2,3,5}, s = {2,3,5}
	fmt.Println(len(s), cap(s)) // print 3 3
	s = append(s, 6)            // a = {1,2,3,5}, s = {2,3,5,6}
	fmt.Println(len(s), cap(s)) // print 4 6
}

当s的len小于cap时,append操作会将修改传递到数组a中,但是当超出容量限制后,append操作触发了s的拷贝。如果有其他slice依赖数组a,那么第一个append操作也会影响其他slice。

7. byte slice and string

字符串底层就是byte切片,两者之间的互转只需要强转即可,这是go为开发者带来的便利,一个值得注意的点是,byte切片中的元素是可修改的,而string中的字符是不可修改的,因此,两者之间的强转,其实都存在空间申请和数据拷贝,所以与c/cpp中的强转不同,这是一个有代价的强转。

func main() {
	b := []byte{'h', 'e', 'l', 'l', 'o'}
	s := string(b)
	b1 := []byte(s)
	fmt.Println(&b[0], &b1[0]) // print 0xc00001609a 0xc0000160d0, maybe different on your computer
}

Conclusion

以上就是对Rob Pike文章学习的一些心得。虽说array和slice是不能再基础的数据结构了,但是如果能了解它的设计初衷和思想,也能窥见大师们的一些思维方式。希望从这个角度能有所收获。
如果觉得太初级了,这篇《Slice Tricks》介绍了slice的一些骚技巧,值得一看

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值