golang中常用数据的内存模型之Slice
golang中常用数据的内存模型之Slice
Slice模型
切片(Slice)可以看作是对数组的一种包装形式.也就是说,切片的实现还是数组.
让我们从创建将起:首先我们声明一个切片
[]string{"test1","test2","test3"}
此时go会创建一个长度为3的字符串数组(注意是数组而不是切片),然后创建一个切片的结构体,包括了三个值,
第一个是指向数组首元素的指针,第二个是切片的长度(切片的长度而不是数组的长度),第三个是数组的长度(准确的
说是切片的容量,在本例中切片的容量和数组的真实长度相同).
看到结构体中的三个内容我想大家应该大致了解了切片到底是如何实现的.下面我们再举一个例子
test := make([]string,10,100)
此时切片结构体中的第二个变量和第三个变量的值是多少呢?是的,分别是10和100.我们来仔细说说这两个值到底
在内存中指代了什么.我们创建了test,make会将我们的第三个参数100作为分配内存的依据,也就是说,实际上我们
分配了一个长度为100个string类型的连续的内存空间.而这个10则是当前test可以直接访问的内存,也就是说,在
编写代码的时候,只有这个10才是对程序员"可见的".只有当我们用append()来添加元素的时候,test的可见长度才
会随之增大.如果我们直接访问test[10],那么会panic.
回到第一个例子,我们通过[]string直接创建切片再进行添加操作会发生什么呢?我们贴一段代码
test := []string{"test"}
println(test)
test = append(test,"test1")
println(test)
[1/1]0xc820041f08
[2/2]0xc82000e400
可以看到,test的内存已经发生了变化.也就是说,如果我们使用append()的时候,切片的长度已经大于我们最初
分配的内存,此时切片会重新分配内存,分配的规则就是将当前长度转换为二进制然后左移一位.
Slice与Array
刚才我们提到过,切片的实现是数组,那么二者之间有什么关系?切片就像是指针,数组则是切片具体指向的结构.
下面我们详细说说.
test := make([]string,1)
println(test)
test = append(test,"test1")
println(test)
test = append(test,"test2")
println(test)
test1 := test
println(test1)
test1 = append(test1,"test3")
println(test1)
test = append(test,"test4")
println(test1[3])
println(test[3])
[1/1]0xc820041f08
[2/2]0xc820070020
[3/4]0xc820072040
[3/4]0xc820072040
[4/4]0xc820072040
test4
test4
我们创建了一个切片test,实际上我们是创建了一个数组作为底层结构,然后创建了一个切片结构体并返回.如果
我们创建一个新的切片test1并将test的值赋给test1,可以看到,在对test1做append()的时候内存并没有变化,
也就是说,这两个切片共享着同一个底层数组.下面的println更是可以说明这点,我们在对test1进行append()之后
test的可访问长度依旧是3,所以当我们对test进行append之后,"test4"会覆盖掉test1中的"test3".
让我们更详细的说一下test和test1的关系.我们在创建test的时候,test负责创建了一个Array,那么经过了两次
append()之后,test的长度为3,容量为4.此时我们新建了一个test1,test1指向的也是Array.然后我们对test1调用
了一次append(),此时Array的第四个位置已经被占用了,此时test1的长度和容量都是4,但是test的长度依旧是3,容量
是4,这意味着Array的第四个位置对test是不可见的.如果我们调用test[3]会panic.因为test的不可见性,所以test并
不会知道第四个位置的状态,那么进行append()的时候它仍然会将数据放入test[3]中.也就是Array的第四个位置.此时
就造成了覆盖.
下面说两点容易出BUG的地方.
- append()函数默认在切片的末尾添加内容,而我们用make创建切片的时候"顺便"初始化了指定长度的内容.也就是说,
append()会绕过这些被初始化的内容在末尾开始添加.我在写程序的时候因为忽略了这一点导致了很大的问题... - 当我们只想返回切片的某一部分的时候,譬如用test[:3]来返回切片的前三个位置,就如之前提到的,如果我们对新建的
切片进行append()操作时会覆盖掉原来切片指向的Array的第四个位置.
append函数
append函数接受两个参数,第一个参数为切片,第二个参数为可变参数,可以是该类型的值.同样我们也可以用[]string{"test"}...
的方式传入另一个切片.而append的函数的返回值也是一个切片.也就是说,append函数并没有修改我们传入的切片,而是建立了一个
新的切片返回.
test1 := append(test,"strings")
我们假设test是一个string类型的切片,长度为2容量为4.那么进行了append操作之后,如果我们打印出test的值发现它并没有变化,
长度依旧为2,而test1则指向与test相同的底层数组,而长度为3.相信讲到这里大家都明白发生了什么
建立切片并指定容量
刚才我们提到过,如果切片操作不当是容易造成覆盖的.比如我想返回某一个切片当中的某一部分元素,那么我对切片就再进行了一次切片操作.
test1 = test[:2]
我们假设test长度为5,容量为10.此时的test1的长度应该为2,那么容量呢?
容量为10.也就是说test1的容量会和test同步.那么此时会发生什么?没错,由于上面提到的对于切片长度所造成的不可见性,对test1使用
append()会造成覆盖,并且我们还可以通过test1 = test1[:cap(test1)]来对test1进行扩展.那么我们就失去了做切片的便捷性和意义了.
此时我们应该怎么做呢.请看下面这行代码
test1 = test[:2:2]
方括号中的第三个参数代表了test1的容量.也就是说,我们强制领test1的容量与长度相同,那么此时我们append()操作就会将test1指向到
一个新的底层数组中去,并且我们也避免了使用cap来对其进行扩展.
copy函数
copy函数比较简单,参数为两个类型相同的切片,函数会将第二个参数中的值复制到第一个参数中去.其中复制的大小则取决与两个参数中较小的
那个.
如果第二个参数较小的话,第一个参数中剩余的部分不会发生改变.
copy函数修改的是底层数组,并没有改变切片的指向.