本文适合初学者阅读
- 5.3 切片
- 切片本身并非动态数组或数组指针. 它内部通过指针引用底屋数组, 设定相关属性将数据读写操作限定在指定区域内.
- 切片本身是只只读对象, 其工作机制类似数据指针的一种包装.
- 可基于数组或数组指针创建切片, 以开始和结束索引位置确定所引用的数组片段. 不支持反向索引, 实际范围是个右半开区间.
- 属性cap表示切片所引用数组片段的真实长度, len用于限定可读写的元素数量, 另外, 数组必须addressable, 否则会引发错误.
- 和数组一样, 切片同样使用索引号访问元素的内容. 超始索引为0, 而非对应的底层数组真实索引位置.
package main
import "fmt"
func main() {
x := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
s := x[2:5]
for i := 0; i < len(s); i++ {
fmt.Printf("%d: %x\n", i, s[i])
}
}
// 结果输出:
// 0: 2
// 1: 3
// 2: 4
这点类似于python的切片, 可以是list或tuple,dict的切片
- 可直接创建切片对象, 无须预先准备数组. 因为是引用类型, 须使用make函数或显式初始化语句, 它会自动完成底层数组内存分配
package main
import "fmt"
func main() {
s1 := make([]int, 3, 5) //指定长度len为3, 容量cap为5, 初始值为0
s2 := make([]int, 6) // 指定长度len为6, 省略容量cap的值, 和len相等, 初始值为0
s3 := []int{10, 20, 8:10} // 按初始元素分配底层数组, 并设置len和cap
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s3, len(s3), cap(s3))
}
// 结果输出:
//[0 0 0] 3 5
// [0 0 0 0 0 0] 6 6
// [10 20 0 0 0 0 0 0 10] 9 9
注意以下两种方式的区别:
package main
func main() {
var a []int // 仅定义了一个[]int类型变量, 没有初始化操作
b := []int{} // 使用初始化表达式完成了全部创建过程
println(a == nil, b == nil)
}
// true false
- 切片不支持比较操作., 就算元素类型支持也不行, 仅能判断是否为nil
- 可获取元素地址, 但不能向数组那样直接用指针访问元素内容
- 切片只是很小的结构体对象, 用来代替数组传参可避免复制开锁. 还有make函数允许在运行期动态指定数组长度, 绕开了数组类型必须使用编译期常量的限制.
reslice(再切片)也就是在切片的基础再次切片
- 不能超出cap, 但不受len限制
package main
import "fmt"
func main() {
d := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := d[2:7]
s2 := s1[:4]
for i := 0;i < len(s2); i++{
s2[i] += 100
}
fmt.Println(d)
fmt.Println(s1)
fmt.Println(s2)
}
// result out:
// [0 1 102 103 104 105 6 7 8 9]
// [102 103 104 105 6]
// [102 103 104 105]
- 利用reslice操作, 很容易就能实现一个栈式数据结构
package main
import (
"errors"
"fmt"
)
func main() {
stack := make([]int, 0, 5) // 初始化, 容量最大为5
push := func(x int) error {
n := len(stack)
if n == cap(stack) {
return errors.New("stack is full")
}
stack = stack[:n+1]
stack[n] = x
return nil
}
pop := func() (int, error) {
n := len(stack)
if n == 0 {
return 0, errors.New("stack is empty")
}
x := stack[n-1]
stack = stack[:n-1]
return x, nil
}
for i := 0; i < 7; i++ {
fmt.Printf("push %d: %v, %v\n", i, push(i), stack)
}
for i := 0; i < 7; i++ {
x, err := pop()
fmt.Printf("pop %d: %v, %v\n", x, err, stack)
}
}
// result out:
// push 0: <nil>, [0]
// push 1: <nil>, [0 1]
// push 2: <nil>, [0 1 2]
// push 3: <nil>, [0 1 2 3]
// push 4: <nil>, [0 1 2 3 4]
// push 5: stack is full, [0 1 2 3 4]
// push 6: stack is full, [0 1 2 3 4]
// pop 4: <nil>, [0 1 2 3]
// pop 3: <nil>, [0 1 2]
// pop 2: <nil>, [0 1]
// pop 1: <nil>, [0]
// pop 0: <nil>, []
// pop 0: stack is empty, []
// pop 0: stack is empty, []
append
- 向切片尾部添加数据, 返回新的切片对象.
package main
import (
"fmt"
)
func main() {
s := make([]int, 0, 5)
s1 := append(s,10)
s2 := append(s1, 20, 30)
fmt.Println(s, len(s), cap(s))
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))
}
// result out:
// [] 0 5
// [10] 1 5
// [10 20 30] 3 5
- 数据被追加到原底层数据.若超出cap限制, 则为新切片对象重新分配数组.
package main
import (
"fmt"
)
func main() {
s := make([]int, 0, 100)
s1 := s[:2:4]
s2 := append(s1, 20, 30, 40, 50, 60, 70)
fmt.Printf("s1: %p: %v\n", &s1[0], s1)
fmt.Printf("s2: %p: %v\n", &s2[0], s2)
fmt.Printf("s data: %v\n", s[:10])
fmt.Printf("s1 cap: %d, s2 cap %d\n", cap(s1), cap(s2))
}
// result out:
// s1: 0xc000082000: [0 0]
// s2: 0xc000084000: [0 0 20 30 40 50 60 70] // 数组地址不同, 确认新分配
// s data: [0 0 0 0 0 0 0 0 0 0] // append并未向原数组写入部分数据
// s1 cap: 4, s2 cap 8 // 新数组是原cap的2倍
- 是超出切片cap限制, 而非底层数组长度限制, 因为cap可小于数组长度
- 新分配数组长度是原cap的2倍, 而非原数组的2倍
- 向nil切片追加数据时, 会为其分配 底层数组内存.
- 正因为存在重新分配底层数组, 应该预留足够多的空间, 避免中途内存分配和数据复制的开销.
copy
在两个切片对象间复制数据, 允许指向同一底层数据, 允许目标区间重叠. 最终所复制长度以较短的切片长度(len)为准.
package main
import (
"fmt"
)
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[5:8]
n := copy(s[4:], s1) // 在同一底层数组的不同区间复制
fmt.Println(n, s)
s2 := make([]int, 6)
n = copy(s2, s) // 在不同数组间复制
fmt.Println(n, s2)
}
// result out:
// 3 [0 1 2 3 5 6 7 7 8 9]
// 6 [0 1 2 3 5 6]
- 如果切片中长时间引用大数组中很小的片段, 则建议新建独立切片, 复制出所需要的数据, 以便原数组内存可被及时回收.