一、Go语言中切片类型出现的原因
切片是一种数据类型,这种数据类型便于使用和管理数据集合。
创建一个100万个int类型元素的数组,并将它传递给函数,将会发生什么?
var array [le6]int
foo(array)
fun foo(array [le6]int){
...
}
在64位架构上,100个int类型的数组需要800万字节,即8M的内存。由于Go语言只有值传递,每次调用函数都需要在栈上分配8M的空间并将数组内容复制进去,这不仅浪费内存而且复制还消耗CPU,当数组较大时复制速度较慢也影响程序使用体验。因此可以只需要传入数组的地址,地址在64为系统上只需要消耗8字节,这样可以更好的利用内存和提升性能,但是由于传入的指针,当函数内部修改了指针的指向内容数组也会发生改变,因此设计了切片来处理数这类数组的共享问题。
二、切片深层解析
面试题
func main() {
s := []int{1, 2, 3}
ss := s[1:]
ss = append(ss, 4)
for _, v := range ss {
v += 10
}
for i := range ss {
ss[i] += 10
}
fmt.Println(s)
}
上面那道面试题是对于切片的考察,首先我们需要明白切片的结构。
slice在Go的运行时库中就是一个C语言动态数组的实现,在$GOROOT/src/pkg/runtime/runtime.h中可以看到它的定
struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
这个结构有3个字段,第一个字段表示array的指针,就是真实数据的指针(这个一定要注意),第二个是表示slice的长度,第三个是表示slice的容量,特别需要注意的是:
slice的长度和容量都不是指针
但我们使用 make([]byte, 5) 创建一个切片变量 s 时,它内部的存储的结构如下:
长度是切片引用的元素数目,容量是底层数组的元素数目(从切片指针开始)。
我们对 s 进行切片,观察切片的数据结构和它引用的底层数组:
s=s[2:4]
切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。
前面创建的切片 s 长度小于它的容量。我们可以增长切片的长度为它的容量:
s = s[:cap(s)]
切片增长不能超出其容量。增长超出切片容量将会导致运行时异常,就像切片或数组的索引超 出范围引起异常一样。同样,不能使用小于零的索引去访问切片之前的元素。
三、切片的创建与使用
刚开始使用切片类型的时候很多人很疑惑这样一个问题:
fun main(){
slice :=[]int{1,2,3}
changeSlice(slice)
fmt.Println("slice:",slice)
}
func changeSlice(s []int){
s=append(s,10)
}
这个问题的输出是: 1 2 3
为什么10没有append到切片里面了?
因为通过函数传递slice作为参数的时候,形参拷贝实参的slice结构,但是由于 array部分是指针因此形参与实参共享底层数组,但是len和cap是会发生拷贝,当形参s进行append的时候,len会发生变化,但是实参的len没变,当输出实参slice的值时,只根据它现在的len进行输出,因此输出1 2 3。同理:
slice:=[]int{1,2,3}
s=slice[0:2]
s.append(s,10)
虽然slice与s同用底层数组,但是slice与s的len不相同,因此输出的slice值与s值也不相同。
创建和初始化切片
1、通过数组创建初始化slice
str :=[5]string{"red","blue","Green","Yellow","Pink"}
slice :=str[:]
使用数组初始化创建切片后,切片会与切片共享底层数组,当修改切片或者数组的值时会相互影响,直到如果对切片添加数据超出cap限制,则会为新切片对象重新分配数组。
2、通过make创建并初始化切片
通过make创建切片需要指定至少出入一个参数,指定切片的长度,如果只指定切片的长度,那么切片的容量与长度相等。也可以分别指定长度与容量,且容量要大于等于长度。
通过make创建的切片会自动初始化slice长度范围内值为0。
面试题
func main() {
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)
}
结果为: 0 0 0 0 0 1 2 3
3、通过切片字面量创建切片
str :=[]string{"red","blue","Green","Yellow","Pink"}
切片的长度与容量会基于初始化提供的元素的个数确定。
使用切片字面量时,可以设置长度和容量,slice:=[]string{99:""},创建长度与容量都是100个元素的切片。
如果在[]运算符里面指定一个值,那么创建是数组而不是切片。
nil与空切片
var slice []int
b:=[]int{}
println(a == nil,b==nil)
结果 true false
前者仅仅定义了一个[]int类型的变量,并未执行初始化操作,而后者初始化表达式完成了全部的创建。
但需要描述一个不存在的切片的时候nil很好使用,常用在函数返回。
空切片在底层数组包含0个元素,没有分配任何空间。表示空集合的时候空切片很好使用。
切片的增长
相对于数组而言,实用切片的一个好处就是可以按需增加切片的容量。Go语言内置的append函数会处理增加长度时所有的操作细节。
使用append时,需要一个被操作的切片和一个要追加的值。函数append调用返回时,会返回一个包含修改结果的新切片。函数append总会增加新切片的长度,而容量有可能会发生改变,也可能不会改变,这取决于被操作切片的可用容量。
如果切片底层数组没有足够的可用容量,append函数会创建一个新的底层数组,将被引用的现有值复制到新数组里,再追加新的值。
slice:=[]int{1,2,3,4}
newSlice :=append(slice,50)
append后,newSlice和slice使用不同的底层数组。
函数append会智能地处理底层数组的容量增长。在切片的容量小于1000个元素时,总是会成倍的增加容量。一旦元素个数超过1000,容量的增长因子会设为1.25。随着增长算法的改变,增长因子有可能会发生改变。
四、可能的“陷阱”
切片操作并不会复制底层的数组。整个数组将被保存在内存中,直到它不再被引用。 有时候可能会因为一个小的内存引用导致保存所有的数据。
例如, FindDigits 函数加载整个文件到内存,然后搜索第一个连续的数字,最后结果以切片方式返回
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
这段代码的行为和描述类似,返回的 []byte 指向保存整个文件的数组。因为切片引用了原始的数组, 导致 GC 不能释放数组的空间;只用到少数几个字节却导致整个文件的内容都一直保存在内存里。
要修复整个问题,可以将感兴趣的数据复制到一个新的切片中:
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}