Go语言切片深层解析

一、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的长度和容量都不是指针

Go语言切片深层解析_第1张图片

但我们使用 make([]byte, 5) 创建一个切片变量 s 时,它内部的存储的结构如下:


Go语言切片深层解析_第2张图片

长度是切片引用的元素数目,容量是底层数组的元素数目(从切片指针开始)。
我们对 s 进行切片,观察切片的数据结构和它引用的底层数组:

s=s[2:4]

Go语言切片深层解析_第3张图片

切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。

前面创建的切片 s 长度小于它的容量。我们可以增长切片的长度为它的容量:

s = s[:cap(s)]

Go语言切片深层解析_第4张图片

切片增长不能超出其容量。增长超出切片容量将会导致运行时异常,就像切片或数组的索引超 出范围引起异常一样。同样,不能使用小于零的索引去访问切片之前的元素。

三、切片的创建与使用

刚开始使用切片类型的时候很多人很疑惑这样一个问题:

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
}

你可能感兴趣的:(Go语言切片深层解析)