虽说 Go 的语法在很大程度上和 PHP 很像,但 PHP 中倒是没有“切片”这个概念的,在学习的过程当中也遇到了一些困惑,遂作此笔记。
困惑1:使用 append 函数为切片追加元素后,切片的容量时变时不变,其扩容机制是什么?
困惑2:更改切片的元素会修改其底层数组中对应的元素。为何有些状况下更改了切片元素,其底层数组元素没有更改?数组
切片能够当作是数组的引用。在 Go 中,每一个数组的大小是固定的,不能随意改变大小,切片能够为数组提供动态增加和缩小的需求,但其自己并不存储任何数据。数据结构
/* * 这是一个数组的声明 */ var a [5]int //只指定长度,元素初始化为默认值0 var a [5]int{1,2,3,4,5} /* * 这是一个切片的声明:即声明一个没有长度的数组 */ // 数组未建立 // 方法1:直接初始化 var s []int //声明一个长度和容量为 0 的 nil 切片 var s []int{1,2,3,4,5} // 同时建立一个长度为5的数组 // 方法2:用make()函数来建立切片:var 变量名 = make([]变量类型,长度,容量) var s = make([]int, 0, 5) // 数组已建立 // 切分数组:var 变量名 []变量类型 = arr[low, high],low和high为数组的索引。 var arr = [5]int{1,2,3,4,5} var slice []int = arr[1:4] // [2,3,4]
切片的长度是它所包含的元素个数。
切片的容量是从它的第一个元素到其底层数组元素末尾的个数。
切片 s 的长度和容量可经过表达式 len(s)
和 cap(s)
来获取。app
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // [0 1 2 3 4 5 6 7 8 9] len=10,cap=10 s1 := s[0:5] // [0 1 2 3 4] len=5,cap=10 s2 := s[5:] // [5 6 7 8 9] len=5,cap=5
Go 提供了内建的 append 函数,为切片追加新的元素。函数
func append(s []T, vs ...T) []Tappend 的结果是一个包含原切片全部元素加上新添加元素的切片。oop
下面分两种状况描述了向切片追加新元素后切片长度和容量的变化。
Example 1:性能
package main import "fmt" func main() { arr := [5]int{1,2,3,4,5} // [1 2 3 4 5] fmt.Println(arr) s1 := arr[0:3] // [1 2 3] printSlice(s1) s1 = append(s1, 6) printSlice(s1) fmt.Println(arr) } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %p %v\n", len(s), cap(s), s, s) }
执行结果以下:学习
[1 2 3 4 5] len=3 cap=5 0xc000082030 [1 2 3] len=4 cap=5 0xc000082030 [1 2 3 6] [1 2 3 6 5]
能够看到切片在追加元素后,其容量和指针地址没有变化,但底层数组发生了变化,下标 3 对应的 4 变成了 6。ui
Example 2:指针
package main import "fmt" func main() { arr := [5]int{1,2,3,4} // [1 2 3 4 0] fmt.Println(arr) s2 := arr[2:] // [3 4 0] printSlice(s2) s2 = append(s2, 5) printSlice(s2) fmt.Println(arr) } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %p %v\n", len(s), cap(s), s, s) }
执行结果以下:code
[1 2 3 4 0] len=3 cap=3 0xc00001c130 [3 4 0] len=4 cap=6 0xc00001c180 [3 4 0 5] [1 2 3 4 0]
而这个切片在追加元素后,其容量和指针地址发生了变化,但底层数组未变。
当切片的底层数组不足以容纳全部给定值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。
Go 中切片的数据结构能够在源码下的 src/runtime/slice.go
查看。
// go 1.3.16 src/runtime/slice.go:13 type slice struct { array unsafe.Pointer len int cap int }
能够看到,切片做为数组的引用,有三个属性字段:长度、容量和指向数组的指针。
向 slice 追加元素的时候,若容量不够,会调用 growslice 函数,
// go 1.3.16 src/runtime/slice.go:76 func growslice(et *_type, old slice, cap int) slice { //...code newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } // 跟据切片类型和容量计算要分配内存的大小 var overflow bool var lenmem, newlenmem, capmem uintptr switch { // ...code } // ...code... // 将旧切片的数据搬到新切片开辟的地址中 memmove(p, old.array, lenmem) return slice{p, old.len, newcap} }
从上面的源码,在对 slice 进行 append 等操做时,可能会形成 slice 的自动扩容。其扩容时的大小增加规则是:
上面的两个例子中,切片的容量均小于 1024 个元素,因此扩容的时候增加因子为 2,每增长一个元素,其容量翻番。
Example2 中,由于切片的底层数组没有足够的可用容量,append() 函数会建立一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值,因此原数组没有变化,不是我想象中的[1 2 3 4 5],
扩容1:切片扩容后其容量不变
slice := []int{1,2,3,4,5} // 建立新的切片,其长度为 2 个元素,容量为 4 个元素 mySlice := slice[1:3] // 使用原有的容量来分配一个新元素,将新元素赋值为 40 mySlice = append(mySlice, 40)
执行上面代码后的底层数据结构以下图所示:
扩容2:切片扩容后其容量变化
// 建立一个长度和容量都为 5 的切片 mySlice := []int{1,2,3,4,5} // 向切片追加一个新元素,将新元素赋值为 6 mySlice = append(mySlice, 6)
执行上面代码后的底层数据结构以下图所示: