内容简介:切片(一个
切片( slice )是对数组一个连续片段的引用,,所以切片是一个引用类型(更类似于 C/C++ 中的数组, Python 中的 list )。因为切片是引用,不需要使用额外的内存存储并且比数组更有效率,所以在 Go 中 切片比数组更常用。
构成
一个 slice 由三个部分构成:指针、长度和容量:
- 指针:指向第一个
slice元素对应的底层数组元素的地址(注意,slice的第一个元素并不一定就是数组的第一个元素) - 长度对应
slice中元素的数目,不能超过容量 - 容量一般是从
slice的开始位置到底层数组的结尾位置,内置的len和cap函数分别返回slice的长度和容量。
创建
数组切片
Slice 本身没有数据,是对底层数组的 view 。多个 slice 之间可以共享底层的数据,并且引用的数组部分区间可能重叠,即一个切片和相关数组的其他切片是共享存储的。相反,不同的数组总是代表不同的存储(数组实际上是切片的构建块)。
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7} // [0 1 2 3 4 5 6 7]
s := arr[2:6] // [2 3 4 5]
// 共享存储
s[0] = 10
fmt.Println(arr) // [0 1 10 3 4 5 6 7]
fmt.Println((s)) // [10 3 4 5]
复制代码
数组和 slice 之间有着紧密的联系,和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度,切片是一个 长度可变的数组。
声明赋值
切片与数组的类型字面量的唯一不同是不包含代表其长度的信息。因此,不同长度的切片值是有可能属于同一个类型的,不同长度的数组值必定属于不同类型。
s := []int{1, 2, 3}
复制代码
使用append
由于值传递的关系,必须接收 append 的返回值:
s = append(s, val) s = append(s, val1,val2, val3) s = append(s, slice...) 复制代码
var s []int
for i :=0; i<10; i++ {
s = append(s, i)
}
fmt.Println(s)
复制代码
使用make
cap 是可选参数: make([]type, len, cap)
s1 := make([]int, 16) s2 := make([]int, 10, 32) 复制代码
扩展阅读 —— new() 和 make() 的区别 : 两者都在堆上分配内存,但是它们的行为不同,适用于不同的类型:
-
new(T)为类型T分配一片内存,初始化为0并且返回类型为*T的内存地址,返回一个指向类型为T,值为0的地址的 指针 。适用于数组、结构体。 -
make(T)返回一个类型为T的初始值,它只适用于3种内建的引用类型:切片、map和channel。
也就是说, new 函数分配内存, make 函数初始化。
特性
长度len
切片元素的个数,使用内置函数 len 获取。
容量cap
数组的容量是其长度,切片的容量是切片的第一个元素到底层数组的最后一个元素的长度。
如果切片操作超出 cap(s) 的上限将导致一个 panic 异常,但是超出 len(s) 则是意味着扩展了 slice ,新 slice 的长度会变大(请见添加元素 - 自动扩展 cap )。
// 数组的容量是其长度
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Println(cap(arr)) // 8
// 切片的容量
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6] // [2 3 4 5]
s2 := s1[3:5] // [5 6],注意,扩展了
fmt.Println(cap(s1)) // 6
fmt.Println(cap(s2)) // 3
复制代码
操作
添加元素
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
s2 := s1[3:5]
s3 := append(s2, 10)
s4 := append(s3, 11)
s5 := append(s4, 12)
fmt.Println(s1) // [2 3 4 5]
fmt.Println(s2) // [5 6],注意,扩展了s1
fmt.Println(s3) // [5 6 10]
fmt.Println(s4) // [5 6 10 11]
fmt.Println(s5) // [5 6 10 11 12]
fmt.Println(arr) // [0 1 2 3 4 5 6 10]
复制代码
观察最后的 arr , 长度仍然是 7 ,说明 s3 的 append 改变了原 array 。 s4 、 s5 的 append 由于超出了底层数组的容量,实际上 s3 、 s4 不再 view 原 arr 了,而是 view 了个新的更加长的 array 。
可见, 添加元素时如果超越了 cap,系统会重新分配更大的底层数组 。如果原数组不再被使用,会被垃圾回收。
自动扩展 cap:
var s []int
for i :=0; i<10; i++ {
s = append(s, i)
fmt.Println(len(s), cap(s))
}
fmt.Println(s)
// 1 1
// 2 2
// 3 4
// 4 4
// 5 8
// 6 8
// 7 8
// 8 8
// 9 16
// 10 16
// [0 1 2 3 4 5 6 7 8 9]
复制代码
删除元素
删除索引为 3 的元素,也可以使用 copy 实现,见模拟 stack 。
arr := [...]int{0, 1, 2, 3, 4, 5}
s := arr[:] // [0 1 2 3 4 5]
s = append(s[:3], s[4:]...)
fmt.Println(s) // [0 1 2 4 5]
复制代码
切片拷贝
copy(dst slice, src slice) :对第一个参数值进行修改。
两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。 copy 函数将返回成功复制的元素的个数,等于两个 slice 中较小的长度,所以我们不用担心覆盖会超出目标 slice 的范围。
s1 := []int{1, 2}
s2 := []int{4, 5, 6}
copy(s1, s2)
fmt.Println(s1) // [4 5]
s1 := []int{1, 1, 1, 1}
s2 := []int{4, 5, 6}
copy(s1, s2)
fmt.Println(s1) // [4 5 6 1]
复制代码
遍历
for-range 结构遍历切片
比较
和数组不同的是, slice 之间不能比较,因此我们不能使用 == 操作符来判断两个 slice 是否含有全部相等元素。
标准库提供了高度优化的 bytes.Equal 函数来判断两个 字节型 slice 是否相等( []byte ),但是对于其他类型的 slice ,我们必须自己展开每个元素进行比较:
func equal(x, y []string) bool {
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}
复制代码
nil
slice 唯一合法的比较操作是和 nil 比较:
if slice1 == nil { /* ... */ }
复制代码
- 一个零值的
slice等于nil - 一个
nil值的slice并没有底层数组 - 一个
nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:] - 一个
nil值的slice的行为和其它任意0长度的slice一样,所有Go语言函数应该同等式对待nil值的slice和0长度的slice。 - 如果需要测试一个
slice是否是空的,使用len(s) == 0来判断,不要用s == nil来判断
var s1 []int
fmt.Println(len(s1), s1 == nil) // 0 true
s2 := []int{}
fmt.Println(len(s2), s2 == nil) // 0 false
s3 := []int(nil)
fmt.Println(len(s3), s3 == nil) // 0 true
复制代码
函数传参
如果一个函数需要对数组操作,最好把参数声明为切片。当调用函数时,把数组分片,传入 切片引用 。
数组元素和
package main
import "fmt"
func sum(a []int) int {
s := 0
for i := range a {
s += i
}
return s
}
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5}
fmt.Println(sum(arr[:])) // 15
var s1 []int
fmt.Println(s1 == nil, sum(s1)) // true 0
s2 := []int{}
fmt.Println(s2 == nil, sum(s2)) // false 0
}
复制代码
反转数组
package main
import "fmt"
func reverse(s []int) {
// for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
// s[i], s[j] = s[j], s[i]
// }
for index, i := range s {
s[index], s[len(s) - i - 1] = s[len(s) - i - 1], s[index]
}
}
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5}
reverse(arr[:])
fmt.Println(arr)
var s1 []int
reverse(s1)
fmt.Println(s1)
s2 := []int{}
reverse(s2)
fmt.Println(s2)
}
复制代码
应用技巧
去除空值
举个例子,去除切片中的空值。注意,输入的 slice 和输出的 slice 共享同一底层数组,这有可能修改了原来的数组。这是重用原来的 slice ,节约了内存。
package main
import (
"fmt"
)
func nonempty(strings []string) []string {
i := 0
for _, s := range strings {
if s != "" {
strings[i] = s
i++
}
}
return strings[:i]
}
func main() {
data := []string{"one", "", "three"}
fmt.Printf("%q\n", nonempty(data)) // ["one" "three"]
fmt.Printf("%q\n", data) // ["one" "three" "three"],改变原数组
}
复制代码
使用 append 函数实现:
func nonempty2(strings []string) []string {
out := strings[:0] // 注意,重用原 slice 的关键
for _, s := range strings {
if s != "" {
out = append(out, s)
}
}
return out
}
复制代码
模拟栈
上面的 nonempty2 函数是用 slice 模拟一个 stack ,最初给定的空 slice 对应一个空的 stack :
插入元素( push ):
stack = append(stack, v) 复制代码
取出最顶部( slice 的最后)的元素:
top := stack[len(stack)-1] 复制代码
通过收缩 stack 弹出栈顶的元素( pop ):
stack = stack[:len(stack)-1] // pop 复制代码
删除 slice 中间的某个元素并保存原有的元素顺序:
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
// 或者
// return append(slice[:i], slice[i+1:]...)
}
复制代码
深入理解
appendInt
append 函数对于理解 slice 底层是如何工作的非常重要。下面是第一个版本的 appendInt 函数,专门用于处理 []int 类型的 slice :
func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen <= cap(x) {
// There is room to grow. Extend the slice.
z = x[:zlen]
} else {
// There is insufficient space. Allocate a new array.
// Grow by doubling, for amortized linear complexity.
zcap := zlen
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x) // a built-in function; see text
}
z[len(x)] = y
return z
}
复制代码
- 先检测
slice底层数组是否有足够的容量来保存新添加的元素 - 如果空间足够,直接在原有底层数组之上扩展
slice,将新添加的y元素复制到新扩展的空间,并返回slice。可见,输入的x和输出的z共享相同的底层数组 - 如果没有足够的增长空间,
appendInt函数则会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。结果z和输入的x引用的将是不同的底层数组 - 为了提高内存使用效率,新分配的数组一般略大于保存
x和y所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操的平均时间是一个常数时间。
append
内置的 append 函数可能比 appendInt 的内存扩展策略更复杂,通常我们并不知道 append 调用是否导致了内存的重新分配,所以也不能确认新的 slice 和原始的 slice 是否引用的是相同的底层数组空间。此时,我们就不能确认在原先的 slice 上的操作是否会影响到新的 slice 。在这种情况下,通常是将 append 返回的结果直接赋值给输入的 slice 变量: s = append(s, val) 。
更新 slice 变量不仅对调用 append 函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用 slice ,需要记住尽管底层数组的元素是间接访问的,但是 slice 对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看, slice 并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型:
type IntSlice struct {
ptr *int
len, cap int
}
复制代码
内置的 append 函数可以追加多个元素,甚至追加一个 slice :
var x []int x = append(x, 1) x = append(x, 2, 3) x = append(x, 4, 5, 6) x = append(x, x...) // append the slice x 复制代码
修改 appendInt ,实现类似 append 函数的功能:
func appendInt(x []int, y ...int) []int {
var z []int
zlen := len(x) + len(y)
// ...expand z to at least zlen...
copy(z[len(x):], y)
return z
}
复制代码
参考目录
以上所述就是小编给大家介绍的《Go 学习笔记(11):切片》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Golang 学习笔记二 数组、切片
- 切片 - Go 语言学习笔记
- Golang学习笔记之切片(slice)
- golang学习笔记10:数组切片Slice
- Go语言学习笔记05--切片slice与字典map
- Go语言快速入门笔记(2)--值类型和引用类型,silce切片,map映射
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Everything Store
Brad Stone / Little, Brown and Company / 2013-10-22 / USD 28.00
The definitive story of Amazon.com, one of the most successful companies in the world, and of its driven, brilliant founder, Jeff Bezos. Amazon.com started off delivering books through the mail. Bu......一起来看看 《The Everything Store》 这本书的介绍吧!