切片是引用类型,不支持直接比较,只能和nil比较
1:底层数据结构
type Slice struct {
Elem *Type // element type
}
cmd/compile/internal/types.NewSlice
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
}
Extra 字段是一个只包含切片内元素类型的结构。也就是说切片内元素的类型都是在编译期间确定的。编译器确定了类型之后,会将类型存储在 Extra
字段中帮助程序在运行时动态获取。
2 SliceHeader
编译期间的slice是上述的形式,但是运行时切片可以由如下的结构体表示
type SliceHeader struct {
Data uintptr //是指向数组的指针;
Len int //当前切片的长度;
Cap int //是当前切片的容量即 Data 数组的大小
}
切片的底层是数组,切片其实是引入了一个抽象层,提供了对数组中部分连续片段的引用,而作为数组的引用,我们可以在运行区间可以修改它的长度和范围。当切片底层的数组长度不足时就会触发扩容,切片指向的数组可能会发生变化,不过在上层看来切片是没有变化的,上层只需要与切片打交道不需要关心数组的变化。
3初始化:
arr[0:3] or slice[0:3] //通过下标的方式获得数组或者切片的一部分;
slice := []int{1, 2, 3} //使用字面量初始化新的切片;
slice := make([]int, 10) //使用关键字 make 创建切片:
使用下标初始化切片不会拷贝原数组或者原切片中的数据,它只会创建一个指向原数组的切片结构体,所以修改新切片的数据也会修改原切片
数组和切片比较:
编译器在编译期间简化了获取数组大小、读写数组中的元素等操作:因为数组的内存固定且连续,多数操作都会直接读写内存的特定位置。但是切片是运行时才会确定内容的结构,所有操作还需要依赖 Go 语言的运行时
4:字面量初始化
当我们使用字面量 []int{1, 2, 3}
创建新的切片时,cmd/compile/internal/gc.slicelit
函数会在编译期间将它展开成如下所示的代码片段:
var vstat [3]int
vstat[0] = 1
vstat[1] = 2
vstat[2] = 3
var vauto *[3]int = new([3]int)
*vauto = vstat //值拷贝 不是 vauto = &vstat
slice := vauto[:]
slice := vauto[:] 执行完之后,改变vstat的值 不会影响到 slice了。。。如果是vauto = &vstat 那么new出来的内存也泄露了,改变了vstat的只 slice 也改变了。。。
- 根据切片中的元素数量对底层数组的大小进行推断并创建一个数组
- 将这些字面量元素存储到初始化的数组中
- 创建一个同样指向
[3]int
类型的数组指针 - 将静态存储区的数组
vstat
赋值给vauto
指针所在的地址 - 通过
[:]
操作获取一个底层使用vauto
的切片
从上边最后一条我们也能看出 [:]
操作是创建切片最底层的一种方法。
5:make方式初始化slice
首先检查切片的大小和容量是否足够小,然后切片是否发生了逃逸,最终在堆上初始化
当切片发生逃逸或者非常大时,运行时需要 runtime.makeslice
在堆上初始化切片,如果当前的切片不会发生逃逸并且切片非常小的时候,make([]int, 3, 4)
会被直接转换成如下所示的代码:
var arr [4]int
n := arr[:3]
6:append 扩容
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
}
}
}
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
如果期望容量大于当前容量的两倍就会使用期望容量;
如果当前切片的长度小于 1024 就会将容量翻倍;
如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量
var arr []int64
arr = append(arr, 1, 2, 3, 4, 5)
当我们执行上述代码时,会触发 runtime.growslice
函数扩容 arr
切片并传入期望的新容量 5,这时期望分配的内存大小为 40 字节;不过因为切片中的元素大小等于sys.PtrSize,所以运行时会调用 runtime.roundupsize
向上取整内存的大小到 48 字节,所以新切片的容量为 48 / 8 = 6。
7:拷贝切片
copy(a,b) 把b拷贝到a里边,如果copy是编译期间 不是在运行时调用的,copy会被转换成下边的代码
n := len(a)
if n > len(b) {
n = len(b)
}
if a.ptr != b.ptr {
memmove(a.ptr, b.ptr, n*sizeof(elem(a)))
}
如果拷贝是在运行时发生的,例如:go copy(a,b),编译器会使用 runtime.slicecopy
替换运行期间调用的 copy
,该函数的实现很简单:
func slicecopy(to, fm slice, width uintptr) int {
if fm.len == 0 || to.len == 0 {
return 0
}
n := fm.len
if to.len < n {
n = to.len
}
if width == 0 {
return n
}
...
size := uintptr(n) * width
if size == 1 {
*(*byte)(to.array) = *(*byte)(fm.array)
} else {
memmove(to.array, fm.array, size)
}
return n
}
无论是编译期间拷贝还是运行时拷贝,两种拷贝方式都会通过 runtime.memmove
将整块内存的内容拷贝到目标的内存区域中:
相比于依次拷贝元素,runtime.memmove
能够提供更好的性能。需要注意的是,整块拷贝内存仍然会占用非常多的资源,在大切片上执行拷贝操作时一定要注意对性能的影响。
-----------------------------------------------------------2021-06-08----------------------------------------------------------
切片是引用类型,都指向了底层的一个数组,切片就是一个框,框住了一块连续的内存
var name []T
name:表示变量名
T:表示切片中的元素类型
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
// fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil比较
切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量
切片的长度就是它元素的个数,切片的容量是底层数组从切片的第一个元素到最后一个元素的数量
a := [5]int{55, 56, 57, 58, 59}
b := a[startIndex:endIndex] //基于数组a创建切片 x,y可以省略不写 **左闭右开**
c := a[1:4] //基于数组a创建切片,包括元素a[1],a[2],a[3]
array[low : high : max]
它会将得到的结果切片的容量设置为max-low
var numbers = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
myslice := numbers[4:6:8]
fmt.Println(myslice, len(myslice), cap(myslice))
myslice = myslice[:cap(myslice)]
fmt.Println(myslice[3])
//[5 6] 2 4
//8
make([]T, size, cap)
T:切片的元素类型
size:切片中元素的数量
cap:切片的容量
**如果cap省略,表示cap和size大小一样**
a := make([]int, 2, 10)
fmt.Println(a) //[0 0]
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //10
切片是引用类型 ,切片不能直接比较
我们不能使用 ==
操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil
比较。 一个nil
值的切片并没有底层数组,一个nil
值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil。。。。
var s1 []int //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
//要检查切片是否为空,请始终使用`len(s) == 0`来判断,而不应该使用`s == nil`来判断
切片的赋值拷贝(浅拷贝)
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
//由于切片是引用类型,所以s1和s2其实都指向了同一块内存地址。修改s2的同时s1的值也会发生变化
fmt.Printf("%p %p\n",s1,&s1) //0xc0000120c0 0xc00000c060
fmt.Printf("%p %p\n",s2,&s2) //0xc0000120c0 0xc00000c080
切片的深拷贝 copy
copy(destSlice, srcSlice []T)
//srcSlice: 数据来源切片
//destSlice: 目标切片
// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
//如果c := make([]int,0,5) copy(c, a) c里边是没有值的 因为c的size为0 不能满足拷贝要求
切片 append添加元素
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
//通过var声明的零值切片可以在append()函数直接使用,无需初始化
var s []int
s = append(s, 1, 2, 3)
//没有必要向下边这么干
s := []int{} // 没有必要初始化
s = append(s, 1, 2, 3)
var s = make([]int) // 没有必要初始化
s = append(s, 1, 2, 3)
扩容操作往往发生在append()
函数调用时,所以我们通常都需要用原变量接收append函数的返回值
sli := make([]int,2,2)
fmt.Println(len(sli),cap(sli)) //2 2
fmt.Printf("%v %p %p %p\n",sli,sli,&sli,&sli[0])//[0 0] 0xc000014090 0xc00000c060 0xc000014090
ma := make(map[string][]int)
ma["sandy"] = sli
sli = append(sli,3)
fmt.Println(len(sli),cap(sli)) //3 4
fmt.Printf("%v %p %p %p\n",sli,sli,&sli,&sli[0])//[0 0 3] 0xc0000120e0 0xc00000c060 0xc0000120e0
扩容前后 sli变量的地址一直都是0xc00000c060,但是存放的值放生了变化从0xc000014090-->0xc0000120e0 ,所以说切片名是一个指针
有坑的题目
func main() {
a := []int{1, 2, 3}
for k, v := range a {
if k == 0 {
a[0], a[1] = 100, 200
fmt.Print(a)
}
a[k] = 100 + v
}
fmt.Print(a)
}
/*
[100 200 3][101 300 103]
*/