一个slice是一个数组某个部分的引用。在内存中,它是一个包含3个域的结构体:指向slice中第一个元素的指针,slice的长度,以及slice的容量。长度是下标操作的上界,如x[i]中i必须小于长度。容量是分割操作的上界,如x[i:j]中j不能大于容量。
数组的slice并不会实际复制一份数据,它只是创建一个新的数据结构,包含了另外的一个指针,一个长度和一个容量数据。
如同分割一个字符串,分割数组也不涉及复制操作:它只是新建了一个结构来放置一个不同的指针,长度和容量。在例子中,对[]int{2,3,5,7,11}
求值操作会创建一个包含五个值的数组,并设置x的属性来描述这个数组。分割表达式x[1:3]
并不分配更多的数据:它只是写了一个新的slice结构的属性来引用相同的存储数据。在例子中,长度为2--只有y[0]和y[1]是有效的索引,但是容量为4--y[0:4]是一个有效的分割表达式。
由于slice是不同于指针的多字长结构,分割操作并不需要分配内存,甚至没有通常被保存在堆中的slice头部。这种表示方法使slice操作和在C中传递指针、长度对一样廉价。Go语言最初使用一个指向以上结构的指针来表示slice,但是这样做意味着每个slice操作都会分配一块新的内存对象。即使使用了快速的分配器,还是给垃圾收集器制造了很多没有必要的工作。移除间接引用及分配操作可以让slice足够廉价,以避免传递显式索引。
其实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
};
在对slice进行append等操作时,可能会造成slice的自动扩容。其扩容时的大小增长规则是:
Go有两个数据结构创建函数:new和make。两者的区别在学习Go语言的初期是一个常见的混淆点。基本的区别是new(T)
返回一个*T
,返回的这个指针可以被隐式地消除引用(图中的黑色箭头)。而make(T, args)
返回一个普通的T。通常情况下,T内部有一些隐式的指针(图中的灰色箭头)。一句话,new返回一个指向已清零内存的指针,而make返回一个复杂的结构。
有一种方法可以统一这两种创建方式,但是可能会与C/C++的传统有显著不同:定义make(*T)
来返回一个指向新分配的T的指针,这样一来,new(Point)得写成make(*Point)。但这样做实在是和人们期望的分配函数太不一样了,所以Go没有采用这种设计。
有时候可能需要使用一些比较tricky的技巧,比如利用make弄一块内存自己管理,或者用cgo之类的方式得到的内存,转换为Go类型使用。
从slice中得到一块内存地址是很容易的:
s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])
从一个内存指针构造出Go语言的slice结构相对麻烦一些,比如其中一种方式:
var ptr unsafe.Pointer
s := ((*[1<<10]byte)(ptr))[:200]
先将ptr
强制类型转换为另一种指针,一个指向[1<<10]byte
数组的指针,这里数组大小其实是假的。然后用slice操作取出这个数组的前200个,于是s
就是一个200个元素的slice。
或者这种方式:
var ptr unsafe.Pointer
var s1 = struct {
addr uintptr
len int
cap int
}{ptr, length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))
把slice的底层结构写出来,将addr,len,cap等字段写进去,将这个结构体赋给s。相比上一种写法,这种更好的地方在于cap更加自然,虽然上面写法中实际上1<<10就是cap。
或者使用reflect.SliceHeader的方式来构造slice,比较推荐这种做法:
var o []byte
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(ptr)