介绍
go slices是go的一种原生类型,可以存储数据序列。slices类似于其它语言的数组,不同的是,相比于其它语言的数组,slices多了一些特殊的特性。
go数组
go slices是建立在数组上的抽象概念,所以在了解slices之前,不妨先了解一下go数组
go数组声明如下
var a [4]int //声明数组时,需要指定数组长度和类型
以上声明表示a为一个长度为4,数组类型为int的数组。数组的长度是固定的,不可变的。这里需要注意的是,数组的长度也是数组类型的一部分,[4]int和[5]int是不同的类型。
数组访问是通过索引访问,a[n]表示访问a中从索引0开始的第n个元素
a[0] = 1 //赋值
i := a[0] //取值 i == 1
数组无需显示初始化,数组在声明后,便可以视为一个长度为n,数组中所有元素为声明类型默认值的数组。
var a [4]int
fmt.Println(a) // [0 0 0 0]
a数组在内存中表示的就是四个连续的int值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bk4s1vyR-1655217408163)(images/go slices/1655186312616.png)]
go的数组变量代表的是数组本身,而不是数组第一个元素的地址。在方法中,go数组参数传入的也是一个数组的拷贝,而非指向数组的指针。
如下所示,运行该方法并不会改变原有数组中a[0]的值
func change(a [4]int) {
a[0] = 100
}
var a [4]int
change(a)
fmt.Println(a) // [0 0 0 0]
如下所示,只有传入数组指针,才可以成功修改a[0]的值
func change(a *[4]int) {
a[0] = 100
}
var a [4]int
change(&a)
fmt.Println(a) // [100 0 0 0]
数组的声明也可以像下面这样
b := [2]string{"Penn", "Teller"}
或者这样
b := [...]string{"Penn", "Teller"}
这两个例子声明的数组b,本质上都是 [2]string 类型
slices
go数组在使用上并不太灵活,所以大多数情况下,代码中都使用的是slices,slices以数组为基础,提供了更强大的功能。
slice声明时无需指定数组长度,声明如下
var a []string
或者直接初始化
letters := []string{"a", "b", "c", "d"}
当然,make方法也可以创建slices
s := make([]byte, 5) //指定slices长度len和类型,cap大小默认和长度len一致
除了指定长度和类型,也可以指定cap大小
s = make([]byte, 5, 10)
这里可能有人会有疑惑,slices的len和cap有什么区别,后续我们会详细解释这个问题,接下来继续看看slices其他的创建和初始化操作
前面说到slices是基于数组的,那么根据数组生成slices也是理所当然的操作
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x
创建新的slices可以针对现有的数组或者slices进行切片操作,具体操作方式为 arr[n:m]
将会得到一个包含数组arr(或者slices)下标[n,m)数据的新slices
其中n和m都可以省略,如果全部省略,那么新slices会获得原先数组或者slices的全部数据,只省略n的话,得到数据范围为[0,m),省略m的话,数据范围为[n,len(arr))
slice internals
slice是数组段的描述,它由指向数组的指针,slice长度(len),以及slice最大容量(cap)三部分构成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GFxuHfds-1655217408164)(images/go slices/1655198298844.png)]
使用make创建出来的slice s,结构类似于下面这样
s := make([]byte, 5)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2cjfqtuZ-1655217408164)(images/go slices/1655198357671.png)]
slice长度len是slice引用的元素数量,slice的容量cap则是底层数组中可包含元素的数量(从slice最开始指向的元素开始计算)。
针对s重新切片,其底层变化如下图,可以看到,len变为2,而cap从数组下标2开始计算,为3。
s = s[2:4]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aP907QM0-1655217408165)(images/go slices/1655198849930.png)]
slice由于其结构的特殊性,所以在使用过程中并不像go数组那样传递拷贝的值,而是拷贝的指向底层数组的指针,因此,针对slice的操作都会影响底层数组的数据。这样也可以得到结论,通过对数组或者slice重新切片得到的slice进行操作,将会影响原先数组和slice
d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}
slices的扩容
如果需要增加slice的容量,可以创建一个新的slice,使得新创建的slice容量是原先slice的两倍,并且将原先slice的底层数组的值赋给新的slice的底层数组,最后,将指向原先slice的指针指向新的slice
t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
for i := range s {
t[i] = s[i]
}
s = t
在上面例子中,for循环的赋值还可以用copy方法来实现,copy方法如下
func copy(dst, src []T) int
使用copy方法,可以将上面的扩容操作简化
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
针对slice的常见操作为给slice后面追加新的值,下面用一个简单的函数来实现这一个功能,AppendByte可以给slice后面追加多个数据,在cap大小不够的情况下自动扩容,最后返回新的slice
func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { // if necessary, reallocate
// allocate double what's needed, for future growth.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}
p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}
当然,go语言也提供了append函数来操作slice
func append(s []T, x ...T) []T
append函数和AppendByte的使用方式相同,且都会自动扩展slice的cap
a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
如果需要将两个slice合并的话,可以使用如下方式
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}
这里要说明一下,slice的默认值为nil,但是和map不同的是,slice不需要显示的初始化,可以在声明之后直接使用append方法来为空的slice追加值而不需担心报错。
使用中可能会出现的坑
前面提到,slice使用过程中并不会拷贝整个数组,而仅是拷贝指向底层数组的指针,那么在使用过程中,如果底层数组过大,而我们使用的slice仅需极少的值的话,就会导致内存浪费
下面的例子展示了这样的一种情况,我们仅需文件中的数字,但是slice指向的底层数组会包含整个文件
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
如果仅需数字的话,可以在获取到slice值时,进行一次copy操作,返回新创建的slice,这样,就避免了内存浪费
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}