内容简介:切片(一个
切片( 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映射
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。