slice表示切片(分片),例如对一个数组进行切片,取出数组中的一部分值。在现代编程语言中,slice(切片)几乎成为一种必备特性,它能够从一个数组(列表)中取出任意长度的子数组(列表),为操做数据结构带来很是大的便利性,如python、perl等都支持对数组的slice操做,甚至perl还支持对hash数据结构的slice。python
但Go中的slice和这些语言的slice不太同样,前面所说的语言中,slice是一种切片的操做,切片后返回一个新的数据对象。而Go中的slice不只仅是一种切片动做,仍是一种数据结构(就像数组同样)。算法
Go中的slice依赖于数组,它的底层就是数组,因此数组具备的优势,slice都有。且slice支持能够经过append向slice中追加元素,长度不够时会动态扩展,经过再次slice切片,能够获得获得更小的slice结构,能够迭代、遍历等。编程
实际上slice是这样的结构:先建立一个有特定长度和数据类型的底层数组,而后从这个底层数组中选取一部分元素,返回这些元素组成的集合(或容器),并将slice指向集合中的第一个元素。换句话说,slice自身维护了一个指针属性,指向它底层数组中的某些元素的集合。数组
例如,初始化一个slice数据结构:数据结构
my_slice := make([]int, 3,5) // 输出slice fmt.Println(my_slice) // 输出:[0 0 0]
这表示先声明一个长度为五、数据类型为int的底层数组,而后从这个底层数组中从前向后取3个元素(即index从0到2)做为slice的结构。架构
以下图:app
每个slice结构都由3部分组成:容量(capacity)、长度(length)和指向底层数组某元素的指针,它们各占8字节(1个机器字长,64位机器上一个机器字长为64bit,共8字节大小,32位架构则是32bit,占用4字节),因此任何一个slice都是24字节(3个机器字长)。编程语言
对上面建立的slice来讲,它的长度为3,容量为5,指针指向底层数组的index=0。函数
能够经过len()函数获取slice的长度,经过cap()函数获取slice的Capacity。ui
my_slice := make([]int,3,5) fmt.Println(len(my_slice)) // 3 fmt.Println(cap(my_slice)) // 5
还能够直接经过print()或println()函数去输出slice,它将获得这个slice结构的属性值,也就是length、capacity和pointer:
my_slice := make([]int,3,5) println(my_slice) // [3/5]0xc42003df10
[3/5]
表示length和capacity,0xc42003df10
表示指向的底层数组元素的指针。
务必记住slice的本质是[x/y]0xADDR
,记住它将在不少地方有助于理解slice的特性。另外,我的建议,虽然slice的本质不是指针,但仍然能够将它看做是一种包含了另外两种属性的不纯粹的指针,也就是说,直接认为它是指针。其实不只slice如此,map也如此。
有几种建立slice数据结构的方式。
一种是使用make():
// 建立一个length和capacity都等于5的slice slice := make([]int,5) // length=3,capacity=5的slice slice := make([]int,3,5)
make()比new()函数多一些操做,new()函数只会进行内存分配并作默认的赋0初始化,而make()能够先为底层数组分配好内存,而后从这个底层数组中再额外生成一个slice并初始化。另外,make只能构建slice、map和channel这3种结构的数据对象,由于它们都指向底层数据结构,都须要先为底层数据结构分配好内存并初始化。
还能够直接赋值初始化的方式建立slice:
// 建立长度和容量都为4的slice,并初始化赋值 color_slice := []string{"red","blue","black","green"} // 建立长度和容量为100的slice,并为第100个元素赋值为3 slice := []int{99:3}
注意区分array和slice:
// 建立长度为3的int数组 array := [3]int{10, 20, 30} // 建立长度和容量都为3的slice slice := []int{10, 20, 30}
因为slice底层是数组,因此可使用索引的方式访问slice,或修改slice中元素的值:
// 建立长度为五、容量为5的slice my_slice := []int{11,22,33,44,55} // 访问slice的第2个元素 print(my_slice[1]) // 修改slice的第3个元素的值 my_slice[2] = 333
因为slice的底层是数组,因此访问my_slice[1]
其实是在访问它的底层数组的对应元素。slice能被访问的元素只有length范围内的元素,那些在length以外,但在capacity以内的元素暂时还不属于slice,只有在slice被扩展时(见下文append),capacity中的元素才被归入length,才能被访问。
当声明一个slice,但不作初始化的时候,这个slice就是一个nil slice。
// 声明一个nil slice var nil_slice []int
nil slice表示它的指针为nil,也就是这个slice不会指向哪一个底层数组。也所以,nil slice的长度和容量都为0。
|--------|---------|----------| | nil | 0 | 0 | | ptr | Length | Capacity | |--------|---------|----------|
还能够建立空slice(Empty Slice),空slice表示长度为0,容量为0,但却有指向的slice,只不过指向的底层数组暂时是长度为0的空数组。
// 使用make建立 empty_slice := make([]int,0) // 直接建立 empty_slice := []int{}
Empty Slice的结构以下:
|--------|---------|----------| | ADDR | 0 | 0 | | ptr | Length | Capacity | |--------|---------|----------|
虽然nil slice和Empty slice的长度和容量都为0,输出时的结果都是[]
,且都不存储任何数据,但它们是不一样的。nil slice不会指向底层数组,而空slice会指向底层数组,只不过这个底层数组暂时是空数组。
可使用println()来输出验证:
package main func main() { var nil_s []int empty_s:= []int{} println(nil_s) println(empty_s) }
输出结果:
[0/0]0x0 [0/0]0xc042085f50
固然,不管是nil slice仍是empty slice,均可以对它们进行操做,如append()函数、len()函数和cap()函数。
能够从slice中继续切片生成一个新的slice,这样能实现slice的缩减。
对slice切片的语法为:
SLICE[A:B] SLICE[A:B:C]
其中A表示从SLICE的第几个元素开始切,B控制切片的长度(B-A),C控制切片的容量(C-A),若是没有给定C,则表示切到底层数组的最尾部。
还有几种简化形式:
SLICE[A:] // 从A切到最尾部 SLICE[:B] // 从最开头切到B(不包含B) SLICE[:] // 从头切到尾,等价于复制整个SLICE
例如:
my_slice := []int{11,22,33,44,55} // 生成新的slice,从第二个元素取,切取的长度为2 new_slice := my_slice[1:3]
注意,截取时"左闭右开"。因此上面new_slice
是从my_slice
的index=1开始截取,截取到index=3为止,但不包括index=3这个元素。因此,新的slice是由my_slice
中的第2个元素、第3个元素组成的新的数据结构,长度为2。
如下是slice切片生成新的slice后的结构:
不难发现,一个底层数组,能够生成无数个slice,且对于new_slice而言,它并不知道底层数组index=0的那个元素。
还能够控制切片时新slice的容量:
my_slice := []int{11,22,33,44,55} // 从第二个元素取,切取的长度为2,容量也为2 new_slice := my_slice[1:3:3]
这时新slice的length等于capacity,底层数组的index=四、5将对new_slice永不可见,即便后面对new_slice进行append()致使底层数组扩容也仍然不可见。具体见下文。
因为多个slice共享同一个底层数组,因此当修改了某个slice中的元素时,其它包含该元素的slice也会随之改变,由于slice只是一个指向底层数组的指针(只不过这个指针不纯粹,多了两个额外的属性length和capacity),实际上修改的是底层数组的值,而底层数组是被共享的。
当同一个底层数组有不少slice的时候,一切将变得混乱不堪,由于咱们不可能记住谁在共享它,经过修改某个slice的元素时,将也会影响那些可能咱们不想影响的slice。因此,须要一种特性,保证各个slice的底层数组互不影响,相关内容见下面的"扩容"。
能够将一个slice拷贝到另外一个slice中。
$ go doc builtin copy func copy(dst, src []Type) int
这表示将src slice拷贝到dst slice,src比dst长,就截断,src比dst短,则只拷贝src那部分。
copy的返回值是拷贝成功的元素数量,因此也就是src slice或dst slice中最小的那个长度。
例如:
s1 := []int{11, 22, 33} s2 := make([]int, 5) s3 := make([]int,2) num := copy(s2, s1) copy(s3,s1) fmt.Println(num) // 3 fmt.Println(s2) // [11,22,33,0,0] fmt.Println(s3) // [11,22]
此外,copy还能将字符串拷贝到byte slice中,由于字符串实际上就是[]byte
。
func main() { s1 := []byte("Hello") num := copy(s1, "World") fmt.Println(num) fmt.Println(s1) // 输出[87 111 114 108 100 32] fmt.Println(string(s1)) //输出"World" }
可使用append()函数对slice进行扩展,由于它追加元素到slice中,因此必定会增长slice的长度。
但必须注意,append()的结果必须被使用。所谓被使用,能够将其输出、能够赋值给某个slice。若是将append()放在空上下文将会报错:append()已评估,但未使用。同时这也说明,append()返回一个新的slice,原始的slice会保留不变。
例如:
my_slice := []int{11,22,33,44,55} new_slice := my_slice[1:3] // append()追加一个元素2323,返回新的slice app_slice := append(new_slice,2323)
上面的append()在new_slice
的后面增长了一个元素2323,因此app_slice[2]=2323
。但由于这些slice共享同一个底层数组,因此2323也会反映到其它slice中。
如今的数据结构图以下:
固然,若是append()的结果从新赋值给new_slice,则new_slice
会增长长度。
一样,因为string的本质是[]byte,因此string能够append到byte slice中:
s1 := []byte("Hello") s2 := append(s1, "World"...) fmt.Println(string(s2)) // 输出:HelloWorld
当slice的length已经等于capacity的时候,再使用append()给slice追加元素,会自动扩展底层数组的长度。
底层数组扩展时,会生成一个新的底层数组。因此旧底层数组仍然会被旧slice引用,新slice和旧slice再也不共享同一个底层数组。
func main() { my_slice := []int{11,22,33,44,55} new_slice := append(my_slice,66) my_slice[3] = 444 // 修改旧的底层数组 fmt.Println(my_slice) // [11 22 33 444 55] fmt.Println(new_slice) // [11 22 33 44 55 66] fmt.Println(len(my_slice),":",cap(my_slice)) // 5:5 fmt.Println(len(new_slice),":",cap(new_slice)) // 6:10 }
从上面的结果上能够发现,底层数组被扩容为10,且是新的底层数组。
实际上,当底层数组须要扩容时,会按照当前底层数组长度的2倍进行扩容,并生成新数组。若是底层数组的长度超过1000时,将按照25%的比率扩容,也就是1000个元素时,将扩展为1250个,不过这个增加比率的算法可能会随着go版本的递进而改变。
实际上,上面的说法应该改一改:当capacity须要扩容时,会按照当前capacity的2倍对数组进行扩容。或者说,是按照slice的本质[x/y]0xADDR
的容量y来判断如何扩容的。之因此要特别强调这两种不一样,是由于很容易搞混。
例如,扩容的对象是底层数组的真子集时:
my_slice := []int{11,22,33,44,55} // 限定长度和容量,且让长度和容量相等 new_slice := my_slice[1:3:3] // [22 33] // 扩容 app_slice := append(new_slice,4444)
上面的new_slice
的容量为2,并无对应到底层数组的最结尾,因此new_slice
是my_slice
的一个真子集。这时对new_slice
扩容,将生成一个新的底层数组,新的底层数组容量为4,而不是10。以下图:
由于建立了新的底层数组,因此修改不一样的slice,将不会互相影响。为了保证每次都是修改各自的底层数组,一般会切出仅一个长度、仅一个容量的新slice,这样只要对它进行任何一次扩容,就会生成一个新的底层数组,从而让每一个slice的底层数组都独立。
my_slice := []int{11,22,33,44,55} new_slice := my_slice[2:3:3] app_slice := append(new_slice,3333)
事实上,这仍是会出现共享的概率,由于没有扩容时,那个惟一的元素仍然是共享的,修改它确定会影响至少1个slice,只有切出长度为0,容量为0的slice,才能彻底保证独立性,但这和新建立一个slice没有区别。
slice和数组其实同样,都是一种值,能够将一个slice和另外一个slice进行合并,生成一个新的slice。
合并slice时,只需将append()的第二个参数后加上...
便可,即append(s1,s2...)
表示将s2合并在s1的后面,并返回新的slice。
s1 := []int{1,2} s2 := []int{3,4} s3 := append(s1,s2...) fmt.Println(s3) // [1 2 3 4]
注意append()最多容许两个参数,因此一次性只能合并两个slice。但能够取巧,将append()做为另外一个append()的参数,从而实现多级合并。例如,下面的合并s1和s2,而后再和s3合并,获得s1+s2+s3
合并后的结果。
sn := append(append(s1,s2...),s3...)
slice是一个集合,因此能够进行迭代。
range关键字能够对slice进行迭代,每次返回一个index和对应的元素值。能够将range的迭代结合for循环对slice进行遍历。
package main func main() { s1 := []int{11,22,33,44} for index,value := range s1 { println("index:",index," , ","value",value) } }
输出结果:
index: 0 , value 11 index: 1 , value 22 index: 2 , value 33 index: 3 , value 44
前面说过,虽然slice实际上包含了3个属性,它的数据结构相似于[3/5]0xc42003df10
,但仍能够将slice看做一种指针。这个特性直接体如今函数参数传值上。
Go中函数的参数是按值传递的,因此调用函数时会复制一个参数的副本传递给函数。若是传递给函数的是slice,它将复制该slice副本给函数,这个副本实际上就是[3/5]0xc42003df10
,因此传递给函数的副本仍然指向源slice的底层数组。
换句话说,若是函数内部对slice进行了修改,有可能会直接影响函数外部的底层数组,从而影响其它slice。但并不老是如此,例如函数内部对slice进行扩容,扩容时生成了一个新的底层数组,函数后续的代码只对新的底层数组操做,这样就不会影响原始的底层数组。
例如:
package main import "fmt" func main() { s1 := []int{11, 22, 33, 44} foo(s1) fmt.Println(s1[1]) // 输出:23 } func foo(s []int) { for index, _ := range s { s[index] += 1 } }
上面将输出23,由于foo()直接操做原始的底层数组,对slice的每一个元素都加1。
因为slice的底层是数组,极可能数组很大,但slice所取的元素数量却很小,这就致使数组占用的绝大多数空间是被浪费的。
特别地,垃圾回收器(GC)不会回收正在被引用的对象,当一个函数直接返回指向底层数组的slice时,这个底层数组将不会随函数退出而被回收,而是由于slice的引用而永远保留,除非返回的slice也消失。
所以,当函数的返回值是一个指向底层数组的数据结构时(如slice),应当在函数内部将slice拷贝一份保存到一个使用本身底层数组的新slice中,并返回这个新的slice。这样函数一退出,原来那个体积较大的底层数组就会被回收,保留在内存中的是小的slice。