[奔跑的 Go] 教程九、深入学习 Go 语言切片(Slice)
切片和数组相似,但是它们在长度上有所不同。
什么是切片
切片和数组相似,它们都是 保存相同数据类型元素的容器 但是切片 元素个数可变。
切片
是由基础数据类型组成,因此它是一种 复合数据类型 (查阅基础数据类型课程)。
除了不需要明确指定元素个数,定义一个切片的与定义一个数组数组的语法十分相似。 因此,s
就是一个切片
var s []int
上面的代码将会创建一个 int
类型的切片,意味着在这个切片里可以保存 int
类型的元素。有一个问题是:切片的零值是什么? 正如我们在数组中看到的那样,数组的零值是对应数据类型的零值。就像一个长度为 n
类型为 int
的数组,将会有 n
个 0 作为其默认值,因为 int
类型对应的零值是 0
。但是在切片 的情况下,像上面定义的切片的零值是nil
。下面的这段代码将会返回 true
。
https://play.golang.org/p/JI6ikCK2f9x
如果你问为什么是 nil
。那是因为 切片只是对一个数组的引用
是不是
*nil*
,*切片*
的类型是*[]Type*
。上面的例子中, 切片*s*
的类型是*[]int*
.
☛ slice
是对数组的引用
这可能听起来很奇怪,但是切片中不存储任何数据,数据是存储在数组中的。你可能会问这怎么可能,数组的长度不是固定的吗?
当 slice
需要扩容时,就会再创建一个合适长度的新数组,以存储更多数据。
当我们用 var s [] int
创建一个slice
时,它还没有引用任何数组,所以,它的值是 nil
。下面来看一下如何引用数组。
创建一个数组,并将该数组中的一些元素复制到切片中。
https://play.golang.org/p/1naC_0qQz_E
在上面的程序中,我们定义了一个类型为 int
的切片 s
,但是这个切片没有引用任何数组。因此,它的值是 nil
, 并且第一个 Println
语句将输出 true
。
之后,我们创建了一个类型为 int
的数组 a
,并为变量 s
赋值 a [2:4]
从而创建一个新的切片。 a [2:4]
的意思是,根据数组 a
生成一个切片,切片的值是取数组 a
中索引 2
到索引 3
的元素。我稍后会解释操作符 [:]
的用法。
现在,由于 s
引用了数组 a
,所以它的值不是 nil
,s
的值应该是 [3,4]
。
既然我们说 slice
是对数组的引用,那么,我们可以尝试修改数组的值,看是否会对 slice
产生影响。
在上面的程序中,对数组 a
的 第三个 和 第四个 元素的值(索引值分别为 2
和 3
)做一下修改,再来看下检查切片 s
的值。
https://play.golang.org/p/9xi8b8TTqHY
从上面的结果中,我们可以得出结论,slice
确实只是对数组的引用,并且对数组的任何更改,都会影响切片的值。
☛ slice
的长度和容量
正如我们在 数组 课程中了解到的,使用 len
函数可以查看数据的长度。这个函数对于切片同样适用。
https://play.golang.org/p/tKJaxdY7dYp
上面这段代码的输出为 “Length of s = 2”,这是正确的,因为 它只引用了数组 ` a ` 中的2个元素。
切片的容量就是他能够容纳元素的数量,go提供内置函数cap来获取这个容量大小
https://play.golang.org/p/eAbelmHUkZK
上面的程序返回7,这就是切片的容量。
因为slice引用一个数组,所以它可以一直引用数组直到结束。由于从上面例子中的索引从2开始,数组中有7个元素,因此数组的容量为7。
这是否意味着我们可以在超出其自身容量的情况下进行切片?是可以的。我们可以使用append函数来实现。
☛ 切片
是一个 结构体
我们在接下来的课程中会学习 结构体
, 但结构体
是由一些不同的类型的字段组成的一种 类型
,这个结构体类型是依赖于这些组成变量的。
切片
结构体如下所示
type slice struct {
zerothElement *type
len int
cap int
}
切片
结构体由指向切片引用数组的首元素的 zerothElement
指针组成的。len
和 cap
分别表示切片的长度和容量。 type
是(引用)数组中内部元素的类型。
因此,当定义新切片时,zerothElement
指针被设置为其零值,即「nil」。 但是当切片引用指向一个数组时,该指针的值将不是「nil」。
我们会在接下来的教程里学到更多关于 指针
的知识。但是下面的例子将显示 a [2]
和 s [0]
的内存地址
是相同的,这意味着他们指向内存中同一个元素。
https://play.golang.org/p/0jUjmjhTCos
0xc420018100
是用 16 进制表示内存里地址的数字。在不同的机器上得到的值可能不一样。
如果切片中元素的值发生改变,数组将会怎样呢? 这是一个非常好的问题。众所周知,切片本身不存储任何数据,而只是对数组的引用。因此,如果切片中元素的值发生改变,那么数组中元素的值也会相应地发生变化。
https://play.golang.org/p/eEChIs0-66G
☛ append
函数
我们可以使用内置函数 append
来向一个切片添加新的元素。 append
函数的格式是:
func append(slice []Type, elems ...Type) []Type
它的第一个参数是一个 slice
, 其他一个或多个参数是要往切片中添加的元素,该函数返回一个和 slice
数据类型一致的新的切片。因此, slice
是一个 variadic function
(我们会在接下来的教程中学习到更多关于可变参数函数的知识)。
既然 append
函数不会改变原来的slice,让我们来看看它是如何工作的。
https://play.golang.org/p/dSA5x7TkFeS
通过上面的运行结果,我们可以看到, s
没有被改变,两个新的元素被拷贝到了 newS
中,但是,我们可以看到数组 a
发生了变化! append
函数改变了 s
切片所引用的数组。
这太可怕了,因此操作 切片 时要特别注意,在使用 append
函数时,尽量像 s = append(s, ...)
这样,对切片本身进行操作,增加代码的可控性。
如果我们在一个切片中添加足够多的元素,直到超过了切片的容量,将会发生什么? 我们来试一试。
https://play.golang.org/p/qKtVAka498Z
首先,我们创建一个 int
类型的数组 a
,并用一组整数进行初始化。然后,使用数组 a
索引 2
到索引 3
之间的元素来创建一个切片 s
。
在第一段 Print
语句中,我们验证了 s
和 a
的值。然后,我们通过匹配它们各自元素的内存地址来确保 s
引用了数组 a
。切片 s
的长度和容量也得到了确认。
然后我们向切片 s
中再添加 7
个元素,我们预计 s
会有 9
个元素,长度为 9
,但是我们暂时还不知道它的容量将会是多大。在接下来的 Print
语句中,我们发现,切片 s
的容量由原来的 7
变成了 14
,同时长度变成了 9
。但是数组 a
没有被改变。
这看起来有点不可思议。当我们在切片中追加元素时,如果切片引用的数组不足以存储新的元素,go就会创建一个更大的数组并把原来的 slice
的值复制进去,然后将 append
的值也添加的新的数组中去,原来的数组不发生任何变化。
☛ anonymous array
切片
迄今为止,我们看到的切片都是引用了一个明确定义的数组,但大多数情况下,你都是与匿名数组打交道。
和数组类似,slice
也可以使用初始值来定义。在本例中,go将先创建包含这些值的匿名数组,然后创建一个相关切片。
https://play.golang.org/p/l_uhlR5KjNY
很明显,这个切片的容量是 6
,go会优先创建长度为 6
的数组因为我们将要创建有 6
个元素的切片。那么,当我们添加两个更多的元素时会发生什么?
https://play.golang.org/p/dmcnLc6Ys8c
这时,go创建了一个长度为 12
的数组,因为当我们将 2
个新元素添加到切片时,原来长度为 6
的数组,已经不足以存储 8
个元素。如果我们再将新的元素添加到切片,那么直到切片的长度超过 12
,都不会再有新的数组被创建。
☛ copy
function
go提供了一个内置函数 copy
来拷贝一个切片到另一个切片。copy
函数的格式如下:
func copy(dst, src []Type) int
dst
是目标切片,src
是源切片. copy
函数将会返回拷贝的元素个数,也是 len(dst)
和 len(src)
的最小值。
https://play.golang.org/p/MkFRMZl-v1B
在上面的程序中,我们定义了一个 nil
切片 s1
,和两个非空的切片 s2
和 s3
。第一个 copy
语句试图将 s2
复制到 s1
,但因为 s1
是一个空切片,所以不会发生任何改变, s1
的值将仍为 nil
。
append
不会是这种情况。因为go已经准备好在需要时创建新的数组,所以在 nil
的切片中 append
将正常工作。
在第二个 copy
语句中,我们要把 s3
复制到 s2
中,因为 s3
包含 4 个元素, s2
包含 3 个元素,所以只有 3 个会被复制(3 和 4 的最小值)。 因为 copy
不会添加新的元素。
在第三个 copy
语句中,我们把 s4
复制到 s3
中。由于 s4
包含 3 个元素,而 s3
包含 4 个元素,因此在 s3
中只会替换 3 个元素。
☛ make
function
在上面的例子中,我们看到 s1
保持不变因为它是一个 nil
切片。但是nil slice
和 empty slice
之间存在差异。nil
切片是引用了一个 不存在的数组 ,而 empty
切片是引用了一个 空数组 或其引用的数组变为了空。
make
是一个内置函数,它可以帮你创建空切片。make
函数的格式如下。make
函数可以创建很多空复合类型。
func make(t Type, size ...IntegerType) Type
在创建 slice
时,make
函数如下所示:
s := make([]type, len, cap)
这里的 type
是切片中元素的类型,len
是切片的长度,cap
是切片的容量。.
让我们来试试前边的例子,其中 s1
是空切片。
https://play.golang.org/p/z0tlrRYLhMu
上面的结果可以证明,一个空切片被创建,同时即使切片的容量较大, copy
函数也不会向切片添加超出其长度的值。
☛ Type...
unpack 操作符
有些人叫它 unpack
操作符或 expand
操作符,我更愿意叫它 spread
,感觉更合乎语义。我们已经知道了 append
函数的作用是,可以将多个元素添加到切片中。如果需求是将一个切片中的元素添加到另一个切片中呢?在这种情况下,我们就需要用到 ...
操作符,因为 append
函数不能接收切片作为被添加的元素,但是可以将切片以数据类型的方式作为参数传递。
https://play.golang.org/p/JfLgynyqVYc
☛ [start:end]
提取操作符
go 中有一个非常棒的操作符 [start:end]
(我喜欢称之为 提取 操作符 ),我们可以用它来提取切片的任何部分。 start
和 end
都是可选的索引。 start
是切片的初始索引,end
表示提取到哪个索引结束,因此提取的内容不包括 end
索引。这样操作会返回一个新切片。
https://play.golang.org/p/lNhNx5KGVrR
在上面的例子中,我们创建了一个从 0
到 9
的整型切片 s
。
-
s [:]
表示从索引 0 开始提取切片s
内的所有元素。因此返回切片s
的所有元素。 -
s[2:]
表示从索引 2 开始提取切片s
内的元素。因此返回[2 3 4 5 6 7 8 9]
。 -
s[:4]
表示从索引 0 开始,到索引 4 为止,提取切片s
内的元素,其中,不包括索引 4 对应的元素。因此返回[0 1 2 3]
。 -
s[2:4]
表示从索引 2 开始,到索引 4 为止,提取切片s
内的元素,其中,不包括索引 4 对应的元素。 因此返回[2 3]
。
要强调的一点是,由提取运算符创建的所有切片,底层引用的都是同一个数组。 可以使用 copy
,make
或 append
函数来避免这个问题。
☛ Slice 遍历
对于 array
和 slice
的遍历是一样的。实际上,slice
就像一个具有相同结构的array
,array
遍历的所有方法都可以用于 slice
。
☛ 引用传递
尽管我们说切片是值传递,但由于它是对同一个底层数组的引用,所以它更像是引用传递。
https://play.golang.org/p/p6O0Uqeww1g
在上面的例子中,我们定义了 makeSquares
方法,它接收一个切片参数,并将切片中的元素做平方运算。这将产生以下结果
[0 1 4 9 16 25 36 49 64 81]
这证明了尽管 slice
是值传递,但是由于本质上它是对同一个底层 array
的引用,因此,我们可以更改这个数组中元素的值。
为什么我们如此确定
slice
是值传递,如果把makeSquares
函数改成func makeSquares(slice [] int){slice = slice [1:5]}
,可以看到此函数并不会改变main
函数中s
的值。
如果我们把上面程序中的参数改为 array
,看看会发生什么。
https://play.golang.org/p/qE8grYQ8Q0s
以上程序的输出为 [0 1 2 3 4 5 6 7 8 9]
,这意味着 makeSquares
函数只是拿到了数组参数的副本。
☛ 删除 切片
中的元素
go 没有提供任何的关键字或函数来直接删除slice
元素。我们需要使用一些技巧来实现。从切片中删除元素就像在需要删除的元素后面和前面连接切片一样。让我们来看看是如何实现的。
https://play.golang.org/p/LfLGN2m-uSm
在上面的程序中,我们从s
中的索引0
开始从s
中提取了一个切片,但不包括索引2
,并从s
中提取索引3
开始到末尾的元素来追加切片。这将创建没有索引“2”的新切片。上面的程序将打印[0 1 3 4 5 6 7 8 9]
。使用相同的技术,我们可以从切片中的任何位置删除多个元素。
☛ 对 slice
进行比较运算
如果尝试运行下面的程序
https://play.golang.org/p/kZ7-SyCBvpt
程序会报错 invalid operation: s1 == s2 (slice can only be compared to nil)
。也就是说,我们只能从整体上判断切片的值是否为 nil
。如果真的需要比较两个切片,可以使用 for range
循环来分别比较切片中的每个元素。
多维切片
和数组类似,切片也可以是多维的。除了没有指明元素个数,定义多维切片与定义多维数组的语法是非常相似的。
s1 := [][]int{
[]int{1, 2},
[]int{3, 4},
[]int{5, 6},
}
s2 := [][]int{
{1, 2},
{3, 4},
{5, 6},
}
☛ 内存优化
正如我们所知,slice 引用的是一个数组。如果有一个函数返回一个切片, 这个切片可能引用一个大的数组.。只要该切片存在内存中, 就不能对数组进行垃圾回收并且占用很大的系统内存。.
下面是一个糟糕的程序
package main
import "fmt"
func getCountries() []string {
countries := []string{"United states", "United kingdom", "Austrilia", "India", "China", "Russia", "France", "Germany", "Spain"} // can be much more
return countries[:3]
}
func main() {
countries := getCountries()
fmt.Println(cap(countries)) // 9
}
我们知道, countries 的容量是 9
意味着 下面的 数组 包含 9 个 元素 (在这个列子中我们是知道的).
为了避免写出上面糟糕的代码(函数返回的切片引用了一个比较大的数组,占用了较多的内存),我们必须创建一个新的匿名数组切片,使其长度固定。下面的程序是个不错的方案。
package main
import "fmt"
func getCountries() (c []string) {
countries := []string{"United states", "United kingdom", "Austrilia", "India", "China", "Russia", "France", "Germany", "Spain"} // can be much more
c = make([]string, 3) // made empty of length and capacity 3
copy(c, countries[:3]) // copied to `c`
return
}
func main() {
countries := getCountries()
fmt.Println(cap(countries)) // 3
}
☛ 阅读更多
正如你所见,go没有提供类似JavaScript中奇特的函数和方法来操作go的切片。我们使用技巧来删除切片元素。如果你正在寻找像pop(从末尾移除),push(从末尾添加),shift(从头部添加)等功能的实现,不妨参考这个网址的内容 :https://github.com/golang/go/wiki/SliceTri...
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: