一、数组
初始化
go
var a [10000]int
a1 := [3]int{1, 2, 3}
a2 := [...]int{1, 2, 3}
第二、第三在运行期间得到的结果是完全相同的,后一种声明方式在编译期间就会被转换成前一种
二、切片
初始化
-
通过下标的方式获得数组或者切片的一部分;
- 最原始也最接近汇编语言的方式
- 如果是切片的一部分,那么指向的是同一个地方
-
使用字面量初始化新的切片;
- 编译期间会在静态区创建数组,创建数组指针,赋值
- 通过[:]获取切片
-
使用关键字
make创建切片:- 需要运行时的参与
- 必须传大小,可选容量
- 小的在栈,大的或发生逃逸的在堆
go
arr[0:3] or slice[0:3]
slice := []int{1, 2, 3}
slice := make([]int, 10)
扩容
go
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
...
newcap := nextslicecap(newLen, oldCap)
...
}
在较新版本的 Go(1.18+)中,扩容策略已经不再是简单的"1024 字节以下翻倍,以上 1.25 倍"。而是:
- 切片新长度大于当前容量的两倍,会使用新长度
- 如果旧容量小于 256,则新容量翻倍。
- 否则平滑过渡到1.25倍逐渐扩容,直到大于等于新长度
go
// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {
newcap := oldCap
doublecap := newcap + newcap
if newLen > doublecap {
return newLen
}
const threshold = 256
if oldCap < threshold {
return doublecap
}
for {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) >> 2
// We need to check `newcap >= newLen` and whether `newcap` overflowed.
// newLen is guaranteed to be larger than zero, hence
// when newcap overflows then `uint(newcap) > uint(newLen)`.
// This allows to check for both with the same comparison.
if uint(newcap) >= uint(newLen) {
break
}
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
return newLen
}
return newcap
}
然而这只是大致容量,下面还需进行内存对齐,为什么?
- 内存管理器的限制 : Go 的内存管理器并不是你想要多少字节就正好给你分配多少。为了减少内存碎片,它预先定义了一系列标准内存块大小,比如 8, 16, 32, 48 ... 32768 字节等。
- 避免浪费 : 如果你计算出来扩容后需要 40 字节,但内存管理器最小只能给你 48 字节的块。与其白白浪费那 8 字节,不如直接把切片的容量(cap)提升到 48 字节所能容纳的数量。
go
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
...
newcap := nextslicecap(newLen, oldCap)
var overflow bool
var lenmem, newlenmem, capmem uintptr
// Specialize for common values of et.Size.
// For 1 we don't need any division/multiplication.
// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
noscan := !et.Pointers()
switch {
// 具体逻辑
}
}
最后收尾工作:看看有没有溢出;申请新内存;数据迁移。
go
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
...
newcap := nextslicecap(newLen, oldCap)
var overflow bool
var lenmem, newlenmem, capmem uintptr
noscan := !et.Pointers()
switch {
...
}
// 溢出
if overflow || capmem > maxAlloc {
panic(errorString("growslice: len out of range"))
}
// 申请新内存
var p unsafe.Pointer
if !et.Pointers() {
// 如果你是一串数字 ([]int):扩容时,Go 只是简单地申请一块内存,把数字拷贝过去。
// GC 扫描时直接跳过这块区域
p = mallocgc(capmem, nil, false)
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// 如果你是一串地址 ([]*User):扩容时,Go 必须非常小心。
// 在拷贝过程中,如果 GC 正在工作,它必须确保每个地址都被正确标记,
// 否则搬家搬到一半,地址指向的对象被 GC 回收了,程序就崩溃了
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
}
}
// 数据迁移
memmove(p, oldPtr, lenmem)
return slice{p, newLen, newcap}
}
三、切片和数组的区别?
- 长度:数组长度固定;长度可变,支持自动扩容
- 声明方式不同
- 内存分配 :数组值类型;切片引用类型,底层引用数组
- 传递开销:数组传递时会复制整个数组;切片仅复制指针、长度和容量