切片简介
切片也是一种数据类型,在Golang中,切片底层基于数组实现的。
我们定义切片如下
var slice []int
切片之所以出现,是为了更好的利用资源,管理数据,如果使用数组,则我们一开始就要定义数组的长度,而使用切片,则可以不需要定义数组长度。
切片数据结构如下,假设初始化分配容量为6,长度为4的切片。
1. 切片的初始化
在初始化切片阶段,会调用下列源码:
// NewSlice returns the slice Type with element type elem.
func NewSlice(elem *Type) *Type {
if t := elem.Cache.slice; t != nil {
if t.Elem() != elem {
Fatalf("elem mismatch")
}
return t
}
t := New(TSLICE)
t.Extra = Slice{Elem: elem}
elem.Cache.slice = t
return t
}
从代码可知,上述方法返回的结构体 TSLICE 中的 Extra 字段是一个只包含切片内元素类型的 Slice{Elem: elem} 结构,也就是说切片内元素的类型是在编译期间确定的。
在源码编译期间的切片是 Slice 类型的,但是在运行时切片由如下的 SliceHeader 结构体表示
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
Data 字段是指向数组的指针,Len 表示当前切片的长度,而 Cap 表示当前切片的容量。其实Cap也是底层数组的大小。正如图片所示,切片只是在数组上面进行了抽象而成的。
2. 切片的访问
访问切片中元素使用的 OINDEX 操作也会在中间代码生成期间转换成对地址的直接访问:
func (s *state) expr(n *Node) *ssa.Value {
switch n.Op {
case OINDEX:
switch {
case n.Left.Type.IsSlice():
p := s.addr(n, false)
return s.load(n.Left.Type.Elem(), p)
...
}
...
}
}
切片的访问操作基本都是在编译期间完成的.
3. 追加元素
往切片追加元素是我们经常的操作。在 Go 语言中我们会使用 append 关键字向切片追加元素。在追加元素过程中,先对切片结构体进行解构获取它的数组指针、大小和容量,如果在追加元素后切片的大小大于容量。在Go语言中,通过append追加元素,但是并不会主动赋值给原切片,需要手动赋值。
var slice []int
slice2 := append(slice, 0)
fmt.Println(slice)
fmt.Println(slice2)
输出:
[]
[0]
并且,Go语言考虑到追加元素超过原切片容量时会进行扩容操作。此处到扩容操作会重新申请一段内存,并将原切片元素拷贝过去。
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
在分配内存空间之前需要先确定新的切片容量,Go 语言根据切片的当前容量选择不同的策略进行扩容:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片容量小于 1024 就会将容量翻倍;
- 如果当前切片容量大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
加一道题目,你觉得输出的会是什么:
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)
}