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
}