slice
数组的长度在定义之后无法再次修改, 数组是值类型, 每次传递都将产生一个副本。虽然这种数据结构无法满足开发者的真实需求。
Go语言提供了数组切片(slice)来弥补数组的不足。
切片并不是数组或数组指针, 它通过内部指针和相关属性引用数组片段, 以实现变长方案。
slice并不涉及真正意义上的动态数组, 而是一个引用类型。slice总是指向一个底层 array, slice的声明也可以像array一样, 只是不需要长度。
array := [...]int{10, 20, 30, 0, 0}
slice := array[0:3:5]
array [0] 10 [1] 20 [2] 30 [3] 0 [4] 0
slice 地址指针 长度3 容量5 整型切片, 长度为3个整型值, 容量为5个整型值
array:是指向下层数组某元素的指针, 该元素也是切片的起始元素;
len:是切片的长度, 即切片中当前元素的个数;
cap:是切片的最大容量, cap >= len。
[low:high:max]
low: 下标的起点
high: 下标的终点(不包含此下标), [a[low], a[high])左闭右开
max: 最大容量(可选), max值不能大于数组的长度, 也不能小于切片的长度(high - low), 默认值为数组的长度。
len = high - low, 长度
cap = max - low, 容量
提醒: len是切片长度, cap实际上是底层数组的长度, max则可以修改slice的容量
切片和数组的区别
package main //必须有个main包import( "fmt")func main(){ //切片和数组的区别 //数组[]里面长度是固定的一个常量, 数组长度不能修改长度, len和cap永远都是5 a := [5]int{} fmt.Printf("len = %d, cap = %d\n", len(a), cap(a)) //切片, []里面为空, 切片的长度或容量可以不固定 s := []int{} fmt.Printf("len = %d, cap = %d\n", len(s), cap(s)) s = append(s, 10) //给切片末尾追加一个成员 append函数只能适用于切片 fmt.Printf("append : len = %d, cap = %d\n", len(s), cap(s))}
切片的创建和初始化
slice和数组的区别: 声明数组时, 方括号内写明了数组的长度或使用...自动计算长度, 而声明slice时, 方括号内没有任何字符。
var s1 []int //声明切片和声明array一样, 只是少了长度, 此为空(nil)切片
s2 := []int{} //简短声明方法, 后面的{}括号不能省略
//make([]T, length, capacity) //length参数不能省略, capacity省略, 则和length的值相同
var s3 []int = make([]int, 0) //创建
s3 = []int{1, 2, 3, 4, 5} //初始化
s4 := make([]int, 0, 0) //简短模式切片创建
s4 = []int{1, 2, 3, 4, 5} //初始化
注意: make 只能创建slice、map 和 channel, 并且返回一个有初始值(非零)。
package mainimport ( "fmt")func main() { //左括号必须和函数名同行 s1 := make([]int, 16) s2 := make([]int, 10, 32) fmt.Println(len(s1), cap(s1)) //16 16 fmt.Println(len(s2), cap(s2)) //10 32}
切片操作
切片截取
s[n] 切片s中索引位置为n的项
s[:] 从切片s的索引位置0到len(s)-1处所获得的切片
s[low:] 从切片s的索引位置low到len(s)-1处所获得的切片
s[:high] 从切片s的索引位置0到high处所获得的切片, len=high
s[low:high] 从切片s的索引位置low到high处所获得的切片, len=high-low
s[low:high:max] 从切片s的索引位置low到high处所获得的切片, len=high-low, cap=max-low
len(s) 切片s的长度, 总是<=cap(s)
cap(s) 切片s的容量, 总是>=len(s)
注意:
low 和 high 是 slice 的索引(index), 其数值必须是整数, max参数用来指定返回的切片的容量
示例说明
array := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
操作 结果 len cap 说明
array[:6:8] [0 1 2 3 4 5] 6 8 省略low
array[5:] [5 6 7 8 9] 5 5 省略high、max
array[:3] [0 1 2] 3 10 省略high、max, 取长度, :3代表从开始位置取三个元素
array[:] [0 1 2 3 4 5 6 7 8 9] 10 10 全部省略
切片与底层数组关系
package main //必须有个main包import( "fmt")func main(){ s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := s[2:5] //[2, 3, 4], 不包括 5, (5-2=3)取三个元素 s1[2] = 100 //修改切片某个元素改变底层数组 fmt.Println(s1, s) //[2 3 100] [0 1 2 3 100 5 6 7 8 9] s2 := s1[2:6] //新切片依旧指向原底层数组 [100 5 6 7] 注意: 2:6 是 s1 切片的索引, 都是从底层拿到的数据, 对应底层 4:8 的 索引, 虽然 5 6 7 在切片 s2 不存在, 但仍然访问底层的数组 s2[3] = 200 fmt.Println(s2) //[100 5 6 200] fmt.Println(s) //[0 1 2 3 100 5 6 200 8 9]}
指针直接访问底层数组, 退化成普通数组操作。
package mainimport "fmt"func main() { s := []int{0, 1, 2, 3} p := &s[2] // *int, 获取底层数组元素指针。 *p += 100 fmt.Println(s) // [0 1 102 3]}
内建函数
append
append函数向slice尾部添加数据, 返回新的slice对象
package main //必须有个main包import( "fmt")func main(){ var s1 []int //创建nil切换 //s1 := make([]int, 0) s1 = append(s1, 1) //追加1个元素 s1 = append(s1, 2, 3) //追加2个元素 s1 = append(s1, 4, 5, 6) //追加3个元素 fmt.Println(s1) //[1 2 3 4 5 6] s2 := make([]int, 5) //[0, 0, 0, 0, 0] s2 = append(s2, 6) fmt.Println(s2) //[0 0 0 0 0 6] s3 := []int{1, 2, 3} s3 = append(s3, 4, 5) fmt.Println(s3) //[1 2 3 4 5]}
使用 "..."追加
package mainimport ( "fmt")func main() { var a = []int{1, 2, 3} fmt.Printf("slice a : %v\n", a) var b = []int{4, 5, 6} fmt.Printf("slice b : %v\n", b) c := append(a, b...) //使用 ... 追加 fmt.Printf("slice c : %v\n", c) d := append(c, 7) fmt.Printf("slice d : %v\n", d) e := append(d, 8, 9, 10) fmt.Printf("slice e : %v\n", e)}
append函数会智能地底层数组的容量增长, 一旦超过原底层数组容量, 通常以2倍容量重新分配底层数组, 并复制原来的数据。
使用make()初始化切片时, length长度最好设置为0, 否则会出现大量无效的值。
注意: append()函数只适合slice切片数据类型的追加。
s := make([]int, 5)
fmt.Println(s) //[0 0 0 0 0]
s = append(s, 6) //[0 0 0 0 0 6]
fmt.Println(s)
package main //必须有个main包import( "fmt")func main(){ s := make([]int, 0, 1) c := cap(s) for i := 0; i < 50; i++ { s = append(s, i) if n := cap(s); n > c { fmt.Printf("cap : %d -> %d\n", c, n) c = n } }}
输出结果:
cap : 1 -> 2
cap : 2 -> 4
cap : 4 -> 8
cap : 8 -> 16
cap : 16 -> 32
cap : 32 -> 64
copy
函数copy在两个slice间复制数据(也可以复制数组), 两个slice可指向同一底层数组。
func copy(dst, src []Type) int
array -> slice 复制
package main //必须有个main包import ( "fmt" "reflect")func main() { data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} //数组的定义方式 s1 := data[8:] //{8, 9} s2 := data[:5] //{0, 1, 2, 3, 4} copy(s2, s1) //dst:s2(目标), src:s1(源) fmt.Println(s2) //[8 9 2 3 4] fmt.Println(data) //[8 9 2 3 4 5 6 7 8 9] fmt.Println(reflect.TypeOf(data), reflect.TypeOf(s2), reflect.TypeOf(s2)) //[10]int []int []int}
array -> slice
package main //必须有个main包import ( "fmt")func main() { data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} //数组的定义方式 s1 := data[8:] //{8, 9} s2 := data[:5] //{0, 1, 2, 3, 4} copy(s1, s2) //dst:s1(目标), src:s2(源) fmt.Println(s1) //[0 1] fmt.Println(data) //[0 1 2 3 4 5 6 7 0 1]}
slice -> slice 复制
package main //必须有个main包import ( "fmt" "reflect")func main() { data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} //数组的定义方式 s1 := data[8:] //{8, 9} s2 := data[:5] //{0, 1, 2, 3, 4} copy(s2, s1) //dst:s2(目标), src:s1(源) fmt.Println(s2) //[8 9 2 3 4] fmt.Println(data) //[8 9 2 3 4 5 6 7 8 9] fmt.Println(reflect.TypeOf(data), reflect.TypeOf(s2), reflect.TypeOf(s2)) //[]int []int []int}
汇总
package main //必须有个main包import ("fmt""reflect")func main() {/*同一底层的数组 此种在实际情况少见*/data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // 切片的定义方式// 第一种 目标的长度(dst) 大于 源(src) 且操作底层同一数组s1 := data[8:] //{8, 9}s2 := data[:5] //{0, 1, 2, 3, 4} 长度为5copy(s2, s1) //dst:s2(目标), src:s1(源) 本质上是操作底层同一个数组fmt.Println(s2) //[8 9 2 3 4] 长度与原目标的长度是一致 5, 源(src)全部覆盖目标(dst)fmt.Println(data) //[8 9 2 3 4 5 6 7 8 9]fmt.Println(reflect.TypeOf(data), reflect.TypeOf(s2), reflect.TypeOf(s2)) //[]int []int []int// 第二种 目标的长度(dst) 小于 源(src) 且操作底层同一数组data2 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // 切片的定义方式s3 := data2[8:] //{8, 9} 长度为 2s4 := data2[:5] //{0, 1, 2, 3, 4}copy(s3, s4) //dst:s3(目标), src:s4(源) 本质上是操作底层同一个数组fmt.Println(s3) // [0 1] 长度与原目标的长度是一致 , 源(src)部分覆盖目标(dst)/*不同底层的数组, 在实际的案例比较多见*/// 第三种 目标的长度(dst) 小于 源(src) 且操作不同的数组s5 := []int{8, 9} // 长度为 2s6 := []int{0, 1, 2, 3, 4} // 定义不同底层的数组copy(s5, s6) //dst:s3(目标), src:s4(源) 如果 dst(目标) 长度小于 src(源) 的长度, 则 copy 源(src)部分;fmt.Println(s5) // [0 1] 长度与原目标的长度是一致 2, 源(src)部分覆盖目标(dst)// 第三种 目标的长度(dst) 大于 源(src) 且操作不同的数组s7 := []int{8, 9}s8 := []int{0, 1, 2, 3, 4} // 长度为 5copy(s8, s7) //dst:s8(目标), src:s7(源) 如果 dst(目标) 长度大于 src(源) 的长度, 则 copy 源(src)全部;fmt.Println(s8) // [8 9 2 3 4] 长度与原目标的长度是一致 5, 源(src)全部覆盖目标(dst)}
注意: copy("目标", "源"), 它只能用于切片, 不能用于 map 等任何其他类型;
它返回结果为一个 int 型值, 表示 copy 的长度;
源(src)不会发生变化(包括长度len和值), 而目标(dst)的长度len始终不会发生变化, 而目标的值被源(src)的值所覆盖, 具体覆盖多少(部分或全部)取决于目标(dst)的长度(len);
如果 dst(目标) 长度小于 src(源) 的长度, 则 copy 部分;
如果 dst(目标) 长度大于 src(源) 的长度, 则 copy 全部;
切片做函数参数
值传递
package main //必须有个main包import( "fmt")func test(s []int){ s[0] = -1 fmt.Println("test : ") s = append(s, 1000) for i, v := range s { fmt.Printf("s[%d] = %d, ", i, v) } // s[0] = -1, s[1] = 1, s[2] = 2, s[3] = 3, s[4] = 4, s[5] = 5, s[6] = 6, s[7] = 7, s[8] = 8, s[9] = 9,s[10] = 1000, fmt.Println("\n") } func main(){ slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} test(slice) fmt.Println("main : ") for i, v := range slice { fmt.Printf("slice[%d] = %d, ", i, v)}//slice[0] = -1, slice[1] = 1, slice[2] = 2, slice[3] = 3, slice[4] = 4, slice[5] = 5, slice[6] = 6, slice[7] = 7, slice[8] = 8, slice[9] = 9,}/*test :s[0] = -1, s[1] = 1, s[2] = 2, s[3] = 3, s[4] = 4, s[5] = 5, s[6] = 6, s[7] = 7, s[8] = 8, s[9] = 9, s[10] = 1000,main :slice[0] = -1, slice[1] = 1, slice[2] = 2, slice[3] = 3, slice[4] = 4, slice[5] = 5, slice[6] = 6, slice[7] = 7, slice[8] = 8, slice[9] = 9,*/
提示: 我们发现采用值传递, 对切片进行修改操作, 执行的结果一致, 因为实际上切片操作是同一底层数组; 但是采用append()追加元素时, 返回的结果却不一致
引用传递
package main //必须有个main包import ( "fmt")func test(s *[]int) {(*s)[0] = -1*s = append(*s, 1000)fmt.Println("test : ")for i, v := range *s {fmt.Printf("s[%d] = %d, ", i, v)}// s[0] = -1, s[1] = 1, s[2] = 2, s[3] = 3, s[4] = 4, s[5] = 5, s[6] = 6, s[7] = 7, s[8] = 8, s[9] = 9, s[10] = 1000,fmt.Println("\n")}func main() { slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} test(&slice) fmt.Println("main : ") for i, v := range slice { fmt.Printf("slice[%d] = %d, ", i, v) } //s[0] = -1, s[1] = 1, s[2] = 2, s[3] = 3, s[4] = 4, s[5] = 5, s[6] = 6, s[7] = 7, s[8] = 8, s[9] = 9, s[10] = 1000,}/*test :s[0] = -1, s[1] = 1, s[2] = 2, s[3] = 3, s[4] = 4, s[5] = 5, s[6] = 6, s[7] = 7, s[8] = 8, s[9] = 9, s[10] = 1000,main :slice[0] = -1, slice[1] = 1, slice[2] = 2, slice[3] = 3, slice[4] = 4, slice[5] = 5, slice[6] = 6, slice[7] = 7, slice[8] = 8, slice[9] = 9, slice[10] = 1000,*/
提示: 我们发现采用引用传递, 对切片进行修改操作, 执行的结果一致, 采用append()追加元素时, 返回的结果一致
注意:
slice 本质上是基于数组实现的, 当传参时, 函数接收到的参数是数组切片的一个复制, 虽然两个是不同的变量, 但是它们都有一个指向同一个地址空间的array指针;
当修改一个数组切片时, 另外一个也会改变, 所以数组切片看起来是引用传递, 其实是值传递;
append()扩容后则使用的不再是同一个内存地址, 因此需要采用引用传递;
结论
(1) append()扩容后则使用的不再是同一个内存地址
(2) 临时变量会开辟新的内存地址, 不会改变原有元素的值
(3) 切片作为函数的参数和返回值时, 一般的情况下没必要使用指针, 除非 append() 扩容的需要;
冒泡法排序切片
package main //必须有个main包import( "fmt" "math/rand" "time")func InitData(s []int){ //设置种子 rand.Seed(time.Now().UnixNano()) for i := 0; i < len(s); i++ { s[i] = rand.Intn(100) //100以内的随机数 } }func BubbleSort(s []int){ n := len(s) for i := 0; i < n; i++ { for j := 0; j < n-1-i; j++ { if( s[j] > s[j+1]){ s[j], s[j+1] = s[j+1], s[j] } } }}func main(){ n := 10 //创建一个切片, len为n s := make([]int, n) InitData(s) //初始化数组 fmt.Println("排序前: ", s) BubbleSort(s) //冒泡排序 fmt.Println("排序后: ", s) } /* 排学前: [93 7 8 94 41 36 16 71 63 25] 排学后: [7 8 16 25 36 41 63 71 93 94]*/
删除切片一个元素
Go语言并没有提供用于删除元素的语法或接口, 而是通过利用切片本身的特性来删除元素——追加元素。即以被删除元素为分界点, 将前后两个部分的内存重新连接起来。
使用切片的追加(append)特性, 利用代码实现。
Slice 删除元素的实现
由于切片没有语法糖实现删除, 因此利用其追加元素的特性完成元素的删除操作;通过内建函数 append() 实现对单个元素以及元素片段的删除。
具体的思路就是
确定删除位置 -> 连接起来 -> 原始切片 -> 将删除前后的元素 -> 产生连接后的切片
package mainimport ( "fmt")func main() { // 初始化一个新的切片 seq seq := []string{"a", "b", "c", "d", "e", "f", "g"} // 指定删除位置 index := 3 // 输出删除位置之前和之后的元素 //fmt.Println(seq[:index], seq[index+1:]) // seq[index+1:]... 表示将后段的整个添加到前段中 // 将删除前后的元素连接起来 seq = append(seq[:index], seq[index+1:]...) // 输出链接后的切片 fmt.Println(seq)}
遍历删除切片所遇到的坑
package mainimport ("fmt")func main() { /* // 最终报错panic: runtime error: index out of range [3] with length 3, 因为range在迭代时已经确定i的范围为[0,len(arr))的左闭右开的区间。 // 但是当满足arr[i] == 3 时对arr进行了修改, 缩短了arr的长度, 此时len(arr)=3, 最大下标为2, 因此当执行arr[3]时会报错。 // 使用 for...rang arr := []int{1, 2, 3, 4} for i := range arr { if arr[i] == 3 { arr = append(arr[:i], arr[i+1:]...) } } fmt.Println(arr) */ // 正确的写法 arr := []int{1, 2, 3, 4} for i := 0; i < len(arr); i++ { fmt.Println(i, arr[i]) if arr[i] == 3 { // 必须采用数组和+下标的方式修改, Go 和 PHP 语法一致 arr = append(arr[:i], arr[i+1:]...) // 删除数组的方式 i-- } } fmt.Println(arr)}
注意: 删除切片中的元素时, 要使用 for ... 循环;
尤其是切片的数量发生变化时, 不能使用 for...range 方式, 容易出现数组越界问题
计算切片中最大值
package mainimport ("fmt")func main() { // 获取最大值 weights := []int{10, 30, 50, 70, 90, 110} var max int // var max int = weights[0] for _, weight := range weights { if weight > max { max = weight } } fmt.Println(max) // 110}
总结:
1. 切片:切片是数组的一个引用, 因此切片是引用类型。但自身是结构体, 值拷贝传递。
2. 切片的长度可以改变, 因此, 切片是一个可变的数组。
3. 切片遍历方式和数组一样, 可以用len()求长度。表示可用元素数量, 读写操作不能超过该限制。
4. cap可以求出slice最大扩张容量, 不能超出数组限制。0 <= len(slice) <= len(array), 其中array是slice引用的数组。
5. 切片的定义:var 变量名 []类型, 比如 var str []string var arr []int。
6. 如果 slice == nil, 那么 len、cap 结果都等于 0。
切片函数常见操作
1 将切片 b 的元素追加到切片 a 之后:a = append(a, b...)
2 复制切片 a 的元素到新的切片 b 上:
b = make([]T, len(a))
copy(b, a)
3 删除位于索引 i 的元素:a = append(a[:i], a[i+1:]...)
4 切除切片 a 中从索引 i 至 j 位置的元素:a = append(a[:i], a[j:]...)
5 为切片 a 扩展 j 个元素长度:a = append(a, make([]T, j)...)
6 在索引 i 的位置插入元素 x:a = append(a[:i], append([]T{x}, a[i:]...)...)
7 在索引 i 的位置插入长度为 j 的新切片:a = append(a[:i], append(make([]T, j), a[i:]...)...)
8 在索引 i 的位置插入切片 b 的所有元素:a = append(a[:i], append(b, a[i:]...)...)
9 取出位于切片 a 最末尾的元素 x:x, a = a[len(a)-1], a[:len(a)-1]
10 将元素 x 追加到切片 a:a = append(a, x)