切片
数组的大小是固定的,在使用时难免会遇到扩容的情况。切片作为数组一个连续片段的引用,它的大小动态可变,我们可以简单将切片理解为动态数组。
切片中持有指向底层存储数据数组的指针,长度指当前切片中存储数据的长度,容量指当前切片的容量,即当前切片从它的第一个数据到其对应数组末尾的长度,可以简单理解为切片在其对应数组中可使用的长度。
声明切片
var name []T
- T 代表切片类型对应的元素类型
切片默认指向一段连续内存区域,可以是数组,也可以是数组本身
从连续区域生成切片是常规的操作,格式:
slice [开始位置:结束位置]
var a = [3]int{1,2,3}
b := a[1,2]
从数据或切片生成的新的切片具有以下特性:
- 取出的元素数量为:结束位置-起始位置;
- 取出元素不包括结束位置对应的索引,切片最后一个元素你用slice[len(slice)]获取;
- 当缺省开始位置时,表示从连续区域开头到结束位置;
- 当缺生结束位置时,表示从开始位置到整个连续区域末尾;
- 两者同时缺省时,与切片本身等效;
- 两者同时为0,等效于空切片,一般用于切片复位;
- 超界会报错
重置切片,清空拥有的元素
a :=[]int{1,2,3}
fmt.Println(a[0:0)
//[]
make()函数制造切片
make([]T,len,cap)
- len : 长度
-
cap : 容量
- 切片不一定必须经过make() 函数才能使用。生成切片、声明后使用append()函数均可以正常使用切片。
append动态添加元素
每个切片会指向一片内存空间,这片空间容纳容量的元素,超过容量,切片就会”扩容”。”扩容”操作往往发生在append调用时。
var car []string
car = append(car,"Old Driver")
//添加多个元素
car = append(car,"ice","Monk")
//添加切片
team := []string{"Pig","Flyingcake","Chicken"}
car = append(car,team...)
fmt.PrintLn(car)
切片的增长规律,参考:https://www.jianshu.com/p/54be5b08a21c
简单的理解如下:
a. 当需要的容量超过原切片容量的两倍时,会使用需要的容量作为新容量。
b. 当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。
切片底层是数组逻辑的实现,切片在扩充容量时,会产生一个新数组
为了避免因为切片是否发生扩容的问题导致bug,最好的处理办法还是在必要时使用 copy 来复制数据,保证得到一个新的切片,以避免后续操作带来预料之外的副作用
复制切片元素到另一个切片
copy()函数,可以迅速的讲一个切片的数据复制到另一个切片空间中。
copy( dest Slice, src Slice []T ) int
copy 的返回值表示实际发生复制的元素个数。
package main
import "fmt"
func main() {
//引用切片数据
ref_Data := src_Data
//预分配足够多的元素切片
copy_data := make([]int,element_Count)
//将数据赋值到新的切片空间中
copy(copy_data,src_Data)
//修改原始数据的第一个元素
src_Data[0] = 999
//打印引用切片的第一个元素
fmt.Println(ref_Data[0])
//打印复制切牌呢的第一个和最后一个元素
fmt.Println(copy_data[0],copy_data[element_Count-1])
// 复制原始数据从4到6(不包含)
copy(copy_data,src_Data[4:6])
for i :=0; i < 5; i++ {
fmt.Printf("%d ",copy_data[i])
}
}
/*
999
0 999
4 5 2 3 4
*/
复制空间 独立空间。
从切片中删除元素
GO并没有对删除切片元素提供的专门语法,需要使用切片本身的特性来删除元素。
package main
import "fmt"
func main() {
seq := []string{"a", "b", "c", "d", "e"}
//指定删除位置
index := 2
//查看删除位置之前的元素和之后的元素
fmt.Println(seq[:index], seq[index+1:])
//将删除点前后的元素连接起来
seq = append(seq[:index], seq[index+1:]...)
fmt.Println(seq)
}
Go中删除元素的本质:是以删除元素为分界点,将前后两个部分的内存重新连接起来。
Array类型的值作为函数参数
作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的:
// 数组使用值拷贝传参
func main() {
x := [3]int{1,2,3}
func(arr [3]int) {
arr[0] = 7
fmt.Println(arr) // [7 2 3]
}(x)
fmt.Println(x) // [1 2 3] // 并不是你以为的 [7 2 3]
}
如果想修改参数数组:
- 直接传递指向这个数组的指针类型:
// 传址会修改原数据 func main() { x := [3]int{1,2,3} func(arr *[3]int) { (*arr)[0] = 7 fmt.Println(arr) // &[7 2 3] }(&x) fmt.Println(x) // [7 2 3] }
- 直接使用 slice:即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据(底层 array)
// 会修改 slice 的底层 array,从而修改 slice func main() { x := []int{1, 2, 3} func(arr []int) { arr[0] = 7 fmt.Println(x) // [7 2 3] }(x) fmt.Println(x) // [7 2 3] }
capacity增长
src/runtime/slice.go:
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
}
}
}
- 先将旧的slice容量乘以2,如果乘以2后的容量仍小于新的slice容量,则取新的slice容量(append多个elems)
- 如果新slice小于等于旧slice容量的2倍,则取旧slice容量乘以2
- 如果旧的slice容量大于1024,则新slice容量取旧slice容量乘以1.25
Qusetion
- 扩容前后的 Slice 是否相同? 情况一: 原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的 数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址 的 Slice。
情况二: 原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区 域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响 原数组。
要复制一个 Slice,最好使用 Copy 函数。