Go的切片slice
约 1947 字大约 6 分钟
作者:程序员CKeen
博客:http://ckeen.cn
长期坚持做有价值的事!积累沉淀,持续成长,升维思考!希望把编码作为长期兴趣爱好😄
切片对数组进行包装,为数据序列提供了更通用、更强大和更方便的接口。Go中的大多数数组编程都是用切片而不是简单的数组来完成的 slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型。
1.slice创建
- 直接通过初始化参数创建
slice0 := []int{1,3,7,5,2,3,4}
fmt.Println(slice0,len(slice0),cap(slice0))
// [1 3 7 5 2 3 4] 7 7
- 使用内置函数make创建。使用默认值进行初始化
slice1 := make([]int,5,10)
fmt.Println(slice1,len(slice1),cap(slice1))
// [0 0 0 0 0] 5 10
- 从指定下标创建。没指定的将使用默认值进行初始化,比如int类型是0值,string类型是空串。
slice2 := [...]int{1:1,12:12}
fmt.Println(slice2)
// [0 1 0 0 0 0 0 0 0 0 0 0 12]
fmt.Println(slice2[0],len(slice2),cap(slice2))
// 0 13 13
2.slice的操作
- 下标索引操作
slice3 := []int{1,3,7,5,2,3,4}
fmt.Println(slice3[1])
// 3
fmt.Println(slice3[10])
// 运行时报panic: runtime error: index out of range [10] with length 7
- for循环操作
slice3 := []int{1,3,7,5,2,3,4}
for _,val := range slice3{
value := val // 复制
fmt.Println(value)
}
- 切片操作。跟python里面切片一样
slice3 := []int{1,3,7,5,2,3,4}
fmt.Println(slice3[2:4])
// [7 5]
- 两个slice不能直接使用 == 进行比较。slice唯一合法的比较操作是和nil比较
slice4 := []int{1,3,7,5,2,3,4}
slice5 := slice4[2:4]
fmt.Println(slice4 == slice5) // 提示:Invalid operation: slice4 == slice5 (operator == is not defined on []int)
fmt.Println(slice4 != nil) // true
- 内置函数的len和cap函数分别返回slice的长度和容量
- 内置函数append进行添加操作
slice6 := []int{1,3,7,5,2,3,4}
slice7 := append(slice6,0)
slice8 := append(slice7,9,8)
fmt.Println(slice7)
// [1 3 7 5 2 3 4 0]
fmt.Println(slice8)
// [1 3 7 5 2 3 4 0 9 8]
fmt.Println(append(slice6,slice7...)) // 两个slice进行添加
// [1 3 7 5 2 3 4 1 3 7 5 2 3 4 0]
- 内置函数copy进行拷贝操作
slice10 := []int{1,3,7,5,2,3,4}
var slice9 = make([]int,len(slice10))
fmt.Println(slice9,len(slice9),cap(slice9))
// [0 0 0 0 0 0 0] 7 7
copy(slice9,slice10)
fmt.Println(slice9,len(slice9),cap(slice9))
// [1 3 7 5 2 3 4] 7 7
3.slice的使用注意点
- 切片保存对底层数组的引用,如果将一个切片分配给另一个切片,则两个切片都引用同一个数组。 修改切片的值,会同时影响到源切片的数据
slice11 := []int{1,3,7,5,2,3,4}
slice12 := slice11[2:5]
slice12[1] = 10
fmt.Println(slice11)
// [1 3 7 10 2 3 4]
fmt.Println(slice12)
// [7 10 2]
- 如果函数接受slice参数,则调用者可以看到它对slice元素所做的更改,类似于传递指向底层数组的指针。
slice13 := []int{1,3,7,5,2,3,4}
reverse(slice13)
fmt.Println(slice13)
// [4 3 2 5 7 3 1]
func reverse(input_slice []int){
slice_len := len(input_slice)
for i := 0 ; i < slice_len / 2; i++ {
input_slice[i], input_slice[slice_len - i - 1] = input_slice[slice_len - i -1], input_slice[i]
}
}
- slice没有提供直接删除的切片某个元素的内置函数,可以自己组合切片来实现
slice13 := []int{1,3,7,5,2,3,4}
slice14 := append(slice13[:2],slice13[4:len(slice13)]...)
fmt.Println(slice14)
// [4 3 7 3 1] 删除下标为3的元素
- 在边界处拷贝 Slices 和 Maps(uber_go_guide)
接收 Slices的时候注意拷贝。当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
trips := ...
d1.SetTrips(trips)
// 这里我们修改 trips[0],但不会影响到 d1.trips
trips[0] = ...
返回 slices的时候注意拷贝。同样,请注意用户对暴露内部状态的 map 或 slice 的修改。
type Stats struct {
mu sync.Mutex
counters map[string]int
}
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
}
// snapshot 现在是一个拷贝
snapshot := stats.Snapshot()
4.slice的runtime的部分实现方式
// slice.go
package runtime
// slice的底层的实现结构
type slice struct {
array unsafe.Pointer // 一个数组的指针
len int
cap int
}
...
// slice的创建
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap)) // uintptr的type的大小 * cap大小的uint指针类型
if overflow || mem > maxAlloc || len < 0 || len > cap {
// 计算有没有溢出
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)// 使用mallockgc进行内存分配, 并返回unsafe.Pointer指针,指向数组
}
// append超过容量的时候,进行容量扩展
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
if et.size == 0 {
// append不应该创建一个带有nil指针但有非零len的切片
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
// 计算容量扩容后新的容量
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap { // 新建的容量大于旧容量的两倍,直接使用当前的容量
newcap = cap
} else {
if old.len < 1024 { // 如果就长度小于1024,直接使用旧的2倍容量
newcap = doublecap
} else {
// 如果旧的容量小于当前容量,则按25%的幅度循环进行增长,直到大于当前容量
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
}
// 创建新的slice的数组
var p unsafe.Pointer
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
}
}
// 将旧的数组复制到新的数组
memmove(p, old.array, lenmem)
// 返回新的slice结构
return slice{p, old.len, newcap}
}
// slice的copy操作
func slicecopy(toPtr unsafe.Pointer, toLen int, fmPtr unsafe.Pointer, fmLen int, width uintptr) int {
if fmLen == 0 || toLen == 0 {
return 0
}
n := fmLen
if toLen < n {
n = toLen
}
if width == 0 {
return n
}
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(slicecopy)
racereadrangepc(fmPtr, uintptr(n*int(width)), callerpc, pc)
racewriterangepc(toPtr, uintptr(n*int(width)), callerpc, pc)
}
if msanenabled {
msanread(fmPtr, uintptr(n*int(width)))
msanwrite(toPtr, uintptr(n*int(width)))
}
size := uintptr(n) * width
if size == 1 { // common case worth about 2x to do here
// 长度为1表示只是一个字节,直接进行赋值
*(*byte)(toPtr) = *(*byte)(fmPtr)
} else {
// 否则使用内存内存拷贝,将源地址数据拷贝到新的数组
memmove(toPtr, fmPtr, size)
}
return n
}
切片动态扩展算法:
1.如果新增的数据个数大于旧切片数据cap的2倍长度,新的切片cap等于旧切片长度+ 新增元素个数的长度,len和cap相同
2.如果新增的数据个数小于旧切片数据cap的2倍长度,并且旧的切片<1024,新的切片cap等于旧切片数据cap的2倍
3.如果新增的数据个数小于旧切片数据cap的2倍长度,并且旧的切片数据>1024,新的切片按1.25的比率进行新增