Go语言初见——Slice之我的浅薄认识
前言
Go语言不愧为21世纪的C语言,有着很多现代语言的特点。它有很多特别的设计,Slice是其中有趣的一个。 作为一个初学者,在这里做些笔记,记录一些浅薄的认识。
切片的本质
go/src/runtime/slice.go 源码
type slice struct {
array unsafe.Pointer
len int
cap int
}
从其源码来看,切片(slice
)是一个结构体,保护长度(len
)、容量(cap
)和一个指向数组(array
)的指针。
- len()获取长度
- cap()获取容量,及最长可以达到多少
- cap >= len,如果不定义cap,默认len=cap
- unsafe.Pointer指针决定了切片是引用类型
由此可见,在Go语言中切片的实现基于数组,换言之,切片是可变的数组。
切片的创建
- 直接声明
var sli1 []int
内容为:
sli1 | len | cap |
---|---|---|
[] | 0 | 0 |
- 使用
new
函数
sli2 := *new([]int)
内容为:
sli2 | len | cap |
---|---|---|
[] | 0 | 0 |
- 通过字面量
sli3 := []int{1, 2, 3}
内容为:
sli3 | len | cap |
---|---|---|
[1 2 3] | 3 | 3 |
- 通过
make
函数
sli4 := make([]int, 5, 10)
内容为:
sli4 | len | cap |
---|---|---|
[0 0 0 0 0 ] | 5 | 10 |
make
的三个参数分别为:切片类型、长度、容量。
- 从切片或数组截取
array := [5]int{1, 3, 5, 7, 9}
sli5 := array[1:5]
sli6 := sli5[1:3]
内容为:
sli5 | len | cap |
---|---|---|
[3 5 7 9] | 4 | 4 |
sli6 | len | cap |
---|---|---|
[5 7] | 2 | 3 |
切片的追加
如何体现切片的动态性,这就要提到切片的追加(append()
)。它可以在原切片基础上追加元素,同时无需考虑追加后的切片长度超出容量,底层实现会进行适当的扩容,以满足切片的动态变化。
nums := make([]int, 3, 4)
fmt.Println(nums, len(nums), cap(nums))
nums = append(nums, 3)
fmt.Println(nums, len(nums), cap(nums))
nums = append(nums, 6)
// 超出cap时,追加元素时会扩容(扩容系数随着原容量增大而减小)
fmt.Println(nums, len(nums), cap(nums))
输出分别为:
[0 0 0] 3 4
[0 0 0 3] 4 4
[0 0 0 3 6] 5 8
这里,切片会在追加操作时自动扩容,其扩容的具体实现见go源码:
go/src/runtime/slice.go
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
......
newcap := oldCap
doublecap := newcap + newcap
if newLen > doublecap {
newcap = newLen
} else {
const threshold = 256
if oldCap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < newLen {
// 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) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = newLen
}
}
}
......
}
总结来看,其扩容倍数是递减的:
oldcap | 扩容系数 |
---|---|
256 | 2.0 |
512 | 1.63 |
1024 | 1.44 |
2048 | 1.35 |
4096 | 1.30 |
这有利于在保证满足切片容量需求的同时,节省了内存。通过计算,我们可以试着求出扩容系数的极限值为1.25(当oldcap远大于256)。
切片的截取
// 截取
num1 := nums[0:3]
nums[0] = 10 // nums元素发生修改,num1也会跟着修改
fmt.Println(nums)
fmt.Println(num1)
fmt.Printf("p-nums:%p, p-num2:%p\n", &nums, &num1) // 切片本身地址
fmt.Printf("p-num-arr:%p, p-num1-arr:%p\n", nums, num1) // 切片是指向数组的指针
fmt.Printf("p-num-arr:%p, p-num1-arr:%p\n", &nums[0], &num1[0]) // 切片指向数组的地址,第一个元素的地址即数组的首地址
输出为:
[10 0 0 3 6]
[10 0 0]
p-num1:0xc0000b6090, p-num2:0xc0000b60f0
p-nums-arr:0xc00009e080, p-num1-arr:0xc00009e080
p-nums-arr:0xc00009e080, p-num1-arr:0xc00009e080
可见,截取的过程是浅拷贝,截取获得的子切片与原切片指向同一块内存空间。 截取的范围是左闭右开的,各种奇技淫巧的写法这里不做讨论,必要时灵活运用即可。
切片的拷贝
那么如何实现深拷贝呢,这就需要用到copy()
了。
num2 := make([]int, 3)
copy(num2, num1)
fmt.Println(num1)
fmt.Println(num2)
fmt.Printf("p-num1:%p, p-num2:%p\n", &num1, &num2) // 切片本身的地址
fmt.Printf("p-num1-arr:%p, p-num2-arr:%p\n", num1, num2) // 切片指向数组的地址
输出为:
[10 0 0]
[10 0 0]
p-num1:0xc0000b60f0, p-num2:0xc0000b6168
p-num1-arr:0xc00009e080, p-num2-arr:0xc0000a0030
拷贝前后的切片及其指向的数组的内存地址都是不同的,说明在执行copy()
的过程中,开启了一个新的内存空间用于存放数组,并开辟一个空间用于存放指向数组的指针,完成深拷贝。
言而总之
切片的功能还是非常强大的,其丰富而灵活的语法可以满足我们对数据集合的操作,而切片作为重要的数据类型也多次被官方修改和优化,这些修改大部分是积极的、趋于便利的、符合当代语言特性的。
转载自:https://juejin.cn/post/7306330038399369256