Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。
简单的说,数组类型的长度是固定的,而切片类型是可变长的。数组的容量永远等于其长度,都是不可变的。
切片的结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的,可以参考下面这段源码。
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
假设原有切片的容量是A,当切片需要扩容的时候:
第一,会先判定需要扩容的容量X,如果X大于两倍的A,那么直接扩容超过X。也就是说新的切片的容量将会是大于等于X。
第二,如果X小等于两倍的A,此时需要进行再一次的判断,旧容量A如果小于1024,直接扩容两倍的A,此时新的切片容量是2A(理论上)。
第三,如果X小等于两倍的A,且旧容量A大于1024,会循环扩容1.25倍,直到大于X为止。此时的新切片的容量是(n 1.25 A)
以上的计算是理论上的值,实际上的值可能会稍微大一些。
具体代码:/runtime/slice.go 中的 growslice方法。
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 //等价于 newcap = 1.25*newcap
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
Go 本身没有切片缩容后,底层数组不会被释放掉。缩容次数多了,会占用很多内存。
可以用copy的方法,创建新的切片和底层数组。并把原来的切片置nil。
准确的说,一个切片不存在底层数组被替换的情况。当一个切片容量不够时,会给他创建一个新的切片,这个切片有自己的底层数组,自己的结构,自己的内存地址。
我们看到某个切片变量被扩容了,实际上是这个变量内容发生了变化。具体而言,该变量的内存地址不变,但是地址里的东西发生了变化。
举一个形象的例子:假设第一次拜访黄河南路1号别墅的时候,这里住了一家三口人。后来发生了一些变化(对应扩容了),我们再一次去拜访这个地址,发现地址没有变化,房子也没有变化,但是变成一家五口人(其中三口人你认识,另外两人你不认识)但这一家人已经不是以前那一家人了。(某些科幻片的设定)
真正会导致底层数据发生变化的只有扩容的时候。因为数组不能被扩容这个缘故,需要重新创建一个新的底层数组,并创建一个新的切片信息。缩容并不会。
a := []int{1,2,3,4,5,6,}
println(a)
//a = a[:3] //裁剪三个
//println(a) //地址不会变化,容量不会变化,长度会变化
a = append(a,make([]int, 5)...) //增加5个元素
println(a) //地址 容量,长度 都会变
如果append时,引发了切片扩容,那么新的切片内容会发生变化,包括底层数组,长度。如果没有触发扩容,那么只有长度会发生变化。具体可以看代码:
s6 := make([]int, 0)
//s6 := make([]int, 1,10)
println(s6)
fmt.Printf("The capacity of s6: %d\n", cap(s6))
for i := 1; i <= 5; i++ {
s6 = append(s6, i)
println(s6) //一旦触发扩容,地址信息会变
fmt.Printf("s6(%d): len: %d, cap: %d\n", i, len(s6), cap(s6)) //长度在稳定增加,但是容量会跳着增加
}
fmt.Println()
切片在range 循环时,value的赋值一样是值传递,本身的地址不变,内容会变。并且循环内对value 的修改,不会影响原来的切片内容。
var b = [6]int{1,2,3,4,5,6,}
for key, value := range b {
fmt.Printf("value的值:%d,value的地址:%x,切片该元素的地址:%x\n", value, &value, &b[key])
//Value的值不会变,value 的地址不变
//println(reflect.TypeOf(value))
value += 1 //验证下会不会改变原
}
fmt.Printf("value的值:%d",b) //值没有加1
var a []int //nil切片,只定义了类型,slice.array内容指向nil。
println(a) //[0/0]0x0
b := make( []int , 0 ) //空切片,有类型,有地址
b := []int{ }
println(b) // [0/0]0xc00003e778
Make 是专门用来创建 slice、map、channel 的值的。它返回的是被创建的值,并且立即可用。
New 是申请一小块内存并标记它是用来存放某个值的。它返回的是指向这块内存的指针,而且这块内存并不会被初始化。或者说,对于一个引用类型的值,那块内存虽然已经有了,但还没法用(因为里面没有针对那个值的数据结构)。
所以,对于引用类型的值,不要用 new,能用 make 就用 make,不能用 make 就用复合字面量来创建。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。