三分钟搞定Slice |Go主题月

672 阅读4分钟

slice直译为切片,和java中的Vector有些类似,在go语言中,slice与数组可谓是相爱相杀,两者区别是数组是固定大小的,slice是可以动态调整大小的,但是slice底层还是基于数组实现的,这就导致go里面的slice有些和预想不一致的地方,还是挺有意思的。

本文目录结构如下:

  • slice用法
    • 声明与初始化
    • 增加数据
    • 复制
    • 裁剪
    • 删除数据
    • 扩展
    • 插入数据
  • slice底层实现
  • slice常见问题
    • slice会导致性能问题吗
    • slice是值传递还是引用传递

slice用法

声明与初始化

slice是使用make关键字初始化的。make关键字只能用于初始化 slice,map,channel,且初始化后的都是空值nil,但这个空值是带有类型的.

var slice1 []type = make([]type, len)
或
slice1 := make([]type, len)

slice也可以在初始化的时候指明容量大小

b := make([]int, 0, 5)

slice可以在声明的同时初始化

s :=[] int {1,2,3 } 

由于slice底层是数组,因此其可以直接从数组初始化

arr := [4]int{1,2,3,4}
s1 := arr[2:3]
s2 := arr[1:]
s3 := arr[:3]

增加数据

a = append(a, b...)

复制

b = make([]T, len(a))
copy(b, a)
// 下面两种方法会慢一些,但是在数据多的时候更有效
b = append([]T(nil), a...)
b = append(a[:0:0], a...)

裁剪

//简单实现,但如果元素是指针,可能导致无法垃圾回收,引发内存泄漏问题
a = append(a[:i], a[j:]...)
//无内存泄漏的方法
copy(a[i:], a[j:])
for k, n := len(a)-j+i, len(a); k < n; k++ {
	a[k] = nil // or the zero value of T
}
a = a[:len(a)-j+i]

删除数据

//简单实现,但如果元素是指针,可能导致无法垃圾回收,引发内存泄漏问题

a = append(a[:i], a[i+1:]...)
或
a = a[:i+copy(a[i:], a[i+1:])]
或
a[i] = a[len(a)-1] //顺序发生变化
a = a[:len(a)-1]

// 无内存泄漏方法
copy(a[i:], a[i+1:])
a[len(a)-1] = nil // or the zero value of T
a = a[:len(a)-1]

扩展

a = append(a[:i], append(make([]T, j), a[i:]...)...)

插入数据

a = append(a[:i], append([]T{x}, a[i:]...)...)
// 上面这个方法中第二个append会创建新的切片,下面的方法可以避免这个问题
s = append(s, 0 /* use the zero value of the element type */)
copy(s[i+1:], s[i:])
s[i] = x

还有一些其他的使用技巧,可以参考Go官方的slice tricks

slice底层实现

slice底层是基于数组实现的,其结构相对简单,具体定义在src/runtime/slice.go

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

属性很直观,指向底层数组的array指针,表示当前长度的len,以及表示总容量的cap

slice常见问题

slice会导致性能问题吗

和其他语言不太一样的是,array指针指向的可以是一个数组的开头,也可以指向数组的其他位置,这也就意味着多个slice可以共享一个底层数组实现。 比如我们如果直接基于已有的数组或切片来创建一个新的slice,那么效率是很高的,原因是根本没有创建新的底层数组,只是创建了一个指针而已。但是这样会带来一个潜在的风险,就是如果原有的数组很大,新创建的slice一直占用,会导致原有的一直无法被回收,比如下面的例子

func lastNumsBySlice(origin []int) []int {
	return origin[len(origin)-2:]
}

func lastNumsByCopy(origin []int) []int {
	result := make([]int, 2)
	copy(result, origin[len(origin)-2:])
	return result
}

两个方法是一样的,都是返回一个slice的最后 2 个元素,但是实现方法一个是直接基于原有的slice,一个是复制出新的。这样会出现的情况是第一个方法会一直占有原slice,导致其无法被垃圾回收,但是第二个方法就不存在这个问题。

slice是值传递还是引用传递

有些文章说slice是引用传递,也有文章说不是,实际上还要从slice的实现结构来分析这个问题,当slice作为参数时,实际上是三个参数*arr, len, cap,所以是否会影响传入参数,还要看底层数组是否发生变化,如果在函数里面发生底层数组变化了,那就不会影响外面的变化,如果没有,则可能影响。比如下面这个例子

package main

import "fmt"

func main() {
	var arr = make([]int,0,10)
	fmt.Println(arr, len(arr), cap(arr), "before append") //[] 0 10 before append
	appendSlice(arr)
	fmt.Println(arr, len(arr), cap(arr), "after append return")  //[] 0 10 after append return
	arr = append(arr, 2)
	arr = append(arr, 2)
	fmt.Println(arr, len(arr), cap(arr), "before change")  // [2 2] 2 10 before change
	changeSlice(arr)
	fmt.Println(arr, len(arr), cap(arr), "after change return")  //[1 2] 2 10 after change return
}

func appendSlice(arr []int) {
	arr = append(arr, 1)
	fmt.Println(arr, len(arr), cap(arr), "after append")  //[1] 1 10 after append
}

func changeSlice(arr []int) {
	arr[0] = 1
	fmt.Println(arr, len(arr), cap(arr), "after change")  //[1 2] 2 10 after change
}

参考资料

Go官方的slice tricks

图形化的slice tricks

A Tour Of Go

切片(slice)性能及陷阱

曹春晖大佬的文章