Go的切片slice

CKeenGolanggo语言基础约 1947 字大约 6 分钟

作者:程序员CKeen
博客:http://ckeen.cnopen in new window

长期坚持做有价值的事!积累沉淀,持续成长,升维思考!希望把编码作为长期兴趣爱好😄


切片对数组进行包装,为数据序列提供了更通用、更强大和更方便的接口。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的比率进行新增