作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。
一.切片(slice)概述
1.数组的局限性
数组的三个特点:
- 1.长度固定;
- 2.连续内存空间;
- 3.同一类型集合;
因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性,比如数组(array)无法实现扩容和缩容。
2.切片(slice)概述
切片(slice)是Golang中一种特有的数据类型,如上图所示, 切片的本质就是对底层数组的封装,它包含了三个信息:
- 1.底层数组的指针;
- 2.切片的长度(len);
- 3.切片的容量(cap);
切片是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。支持自动扩容。切片的三个特点:
- 1.长度可变;
- 2.连续内存空间;
- 3.同一类型集合;
切片是数组一个连续片段的引用,所以切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。
这个片段可以是整个数组,或者由起始和终止索引标识符的一些项的子集,终止索引标识的项不包括在切片内。
切片和数组的区别:
- 1.切片长度不固定,可以根据需求自动扩容,数组长度是固定的;
- 2.数组的长度和容量相等切不可变,而切片的长度和容量并不一定相等;
3.切片的内存分析
package main
import "fmt"
func main() {
// 定义数组
var intArray [5]uint8 = [5]uint8{1, 3, 5, 7, 9}
// 切片构建在数组之上,如果基于数组的索引取切片一定要注意口诀: "前包后不包"。
var slice []uint8 = intArray[1:4]
fmt.Printf("intArray数组: %v, 长度: %d, 容量: %d\n", intArray, len(intArray), cap(intArray))
fmt.Printf("slice切片: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))
fmt.Printf("intArray[1]数组的内存地址: %p\n", &intArray[1])
fmt.Printf("slice[0]切片的内存地址: %p\n", &slice[0])
fmt.Printf("slice[1]切片的内存地址: %p\n", &slice[1])
// 修改切片的数据
slice[1] = 88
// 查看数组和切片的数据是否修改
fmt.Printf("intArray数组: %v, 长度: %d, 容量: %d\n", intArray, len(intArray), cap(intArray))
fmt.Printf("slice切片: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))
}
二.切片的三种定义方式
1.切片表达式(基于已经存在的数组来创建切片)
package main
import "fmt"
func main() {
/*
切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。
切片表达式有两种变体:(省略了low则默认为0,省略了high则默认为切片操作数的长度)
(1)一种指定low和high两个索引界限值的简单的形式;
array[low:high]
(2)另一种是除了low和high索引界限值外还指定容量的完整的形式;
array[low:high:max]
完整切片表达式需要满足的条件是0 >= low >= high >= max >= cap(a),其他条件和简单切片表达式相同;
当设置了max时,则切片的容量设置为"max-low";
*/
a := [5]int{1, 2, 3, 4, 5}
s1 := a[2:] // 等同于 a[2:len(a)]
s2 := a[:3] // 等同于 a[0:3]
s3 := a[:] // 等同于 a[0:len(a)]
s4 := a[1:3:5]
s5 := a[1:3:3]
fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
fmt.Printf("s4:%v len(s4):%v cap(s4):%v\n", s4, len(s4), cap(s4))
fmt.Printf("s5:%v len(s5):%v cap(s5):%v\n", s5, len(s5), cap(s5))
}
2.通过make指令创建切片
package main
import "fmt"
func main() {
/*
如果需要动态的创建一个切片,我们就需要使用内置的make()函数,格式如下:
make([]T, size, cap)
通过make函数常见切片需要传入三个参数:
T:
切片的类型
size:
切片的长度
cap:
切片的容量,容量并不会影响当前元素的个数。
make创建切片本质上就是在底层创建了一个数组,该数组对外不可见,所以不可以直接操作这个数组,要通过切片去间接的访问各个元素。
*/
slice := make([]int, 4, 20)
// 为切片赋值
slice[1] = 100
slice[3] = 200
fmt.Printf("切片的长度:%d,容量:%d,数据:%v\n", len(slice), cap(slice), slice)
}
3.声明切片类型
package main
import (
"fmt"
)
func main() {
// 声明切片类型,定义一个切片,直接就指定具体数组,使用原理类似make
var (
// 声明一个字符串切片
bigdata = []string{"hadoop", "spark", "flink", "kudu", "hbase", "hive"}
// 声明一个整型切片并初始化
scores = []int{99, 88, 77}
// 声明一个布尔切片并初始化
svip = []bool{false, true}
)
fmt.Printf("bigdata切片的长度:[%d],容量:[%d],数据:%v\n", len(bigdata), cap(bigdata), bigdata)
fmt.Printf("scores切片的长度:[%d],容量:[%d],数据:%v\n", len(scores), cap(scores), scores)
fmt.Printf("svip切片的长度:[%d],容量:[%d],数据:%v\n", len(svip), cap(svip), svip)
}
三.切片的遍历
1.基于for循环遍历
package main
import (
"fmt"
)
func main() {
s := []byte{'A', 'B', 'C'}
// 支持基于索引遍历
for i := 0; i < len(s); i++ {
fmt.Printf("第[%d]个索引存储的数据是: [%c]\n", i, s[i])
}
}
2.基于for-range循环遍历
package main
import (
"fmt"
)
func main() {
s := []byte{'A', 'B', 'C'}
// 支持基for-range循环
for index, value := range s {
fmt.Printf("第[%d]个索引对应的数据为:%c\n", index, value)
}
}
四.切片的扩容
1.通过append函数扩容切片
package main
import "fmt"
func main() {
var (
// 1.定义数组
intArr [5]int = [5]int{1, 3, 5, 7, 9}
// 2.定义切片
s1 []int = intArr[1:4]
)
/*
切片扩容的底层原理:
- 1.底层追加元素的时候对数组进行扩容,老数组扩容为新数组;
- 2.创建一个新数组,将老数组中的s1("3","5","7")复制到新数组中,在新数组中追加"66","88";
- 3.s2底层数组指向的是新数组;
- 4.往往我们在使用追加的时候起始想要做的效果给s1追加;
- 5.底层的新数组不能直接维护,需要通过切片简洁维护操作;
*/
s2 := append(s1, 66, 88)
fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
s3 := []int{22, 33, 44}
// append也支持将一个切片直接追加到另一个切片中
s1 = append(s1, s3...)
fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
}
2.切片自动扩容
package main
import (
"fmt"
)
func main() {
var numSlice []int
fmt.Printf("未添加任何元素:%p 数据: %v 长度:%d 容量:%d \n", numSlice, numSlice, len(numSlice), cap(numSlice))
for i := 0; i < 10; i++ {
/*
温馨提示:
- 1.每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。
- 2.当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。
- 3.“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
*/
numSlice = append(numSlice, i)
fmt.Printf("内存地址指针:%p 数据: %v 长度:%d 容量:%d \n", numSlice, numSlice, len(numSlice), cap(numSlice))
}
}
3.切片的扩容策略[了解即可]
可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
从上面的代码可以看出以下内容:
- 1.首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap),否则走else语句继续判断;
- 2.如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),否则走else语句继续判断;
- 3.如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap);
- 5.如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap);
温馨提示:
切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。
五.切片使用注意事项
1.切片使用注意事项
- 1.切片定义后不可以直接使用,需要让其引用到一个数组,或者make一个空间供切片来使用;
- 2.切片使用不能越界;
- 3.切片表达式支持简写形式
案例1:
"var s1 = arr[0:end]"简写为:"var s1 = arr[:end]"
案例2:
"var s2 = arr[start:len(arr)]"简写为"var s2 = arr[start:]"
案例3:
"var s3 = arr[0:len(arr)]简写为"var s3 = arr[:]"
- 4.切片可以继续切片;
- 5.切片可以动态增长,通过"append"函数操作即可;
- 6.切片也支持使用内置函数copy进行拷贝;
- 7.切片是引用类型,不支持直接比较,切片唯一合法的比较操作是和nil比较
1.切片的赋值拷贝
package main
import (
"fmt"
)
func main() {
s1 := make([]int, 3)
// 切片是引用类型,将s1直接复制给s2,此时s1和s2底层共用同一个数组
s2 := s1
fmt.Printf("修改前: s1 --->[%v]\n", s1)
fmt.Printf("修改前: s2 --->[%v]\n", s2)
// 注意,我修改的是s2,并没有取修改s1哈
s2[1] = 100
// 再次查看s1和s2时你会发现二者都发生了变化哟
fmt.Printf("修改后: s1 --->[%v]\n", s1)
fmt.Printf("修改后: s2 --->[%v]\n", s2)
}
2.切片的复制
package main
import (
"fmt"
)
func main() {
s1 := []int{11, 22, 33, 44, 55}
s2 := s1
// 切片是引用类型,所以s1和s2其实都指向了同一块内存地址。修改s2的同时s1的值也会发生变化。
s2[1] = 100
fmt.Printf("修改前: s1 ---> 内存地址: %p [%v], \n", s1, s1)
fmt.Printf("修改前: s2 ---> 内存地址: %p [%v]\n", s2, s2)
// 注意,使用make函数对切片进行初始化操作,此操作在"go1.19.3"版本中对s1和s2指向的数组值也有影响哟~
s3 := make([]int, 3)
// Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:
// copy(destSlice, srcSlice []T)
//
// 温馨提示:
// srcSlice:
// 数据来源切片
// destSlice:
// 目标切片
copy(s1, s3) // 使用copy()函数将切片s1中的元素复制到切片s3,属于值拷贝。
// 由于s3属于只是使用了内置函数copy对s1进行了值拷贝,因此修改s3并不会影响到s1和s2哟~
s3[2] = 200
fmt.Printf("s1 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s1, s1, len(s1), cap(s1))
fmt.Printf("s2 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s2, s2, len(s2), cap(s2))
fmt.Printf("s3 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s3, s3, len(s3), cap(s3))
}
3.切片元素删除
package main
import (
"fmt"
)
func main() {
numberList := []int{11, 22, 33, 44, 55}
fmt.Printf("删除前 ---> numberList: %v\n", numberList)
// Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
// 要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)
numberList = append(numberList[:3], numberList[4:]...) // 删除索引为3的元素
fmt.Printf("删除后 ---> numberList: %v\n", numberList)
}
4.切片不能直接比较
package main
import (
"fmt"
)
func main() {
var a = []bool{false, true}
var b = []bool{false, true}
fmt.Printf("a ---> %v\n", a)
fmt.Printf("b ---> %v\n", b)
// 切片是引用类型,不支持直接比较,切片唯一合法的比较操作是和nil比较。
// fmt.Println(a == b) // 报错: invalid operation: a == b (slice can only be compared to nil)
}
5.判断切片是否为空
package main
import (
"fmt"
)
func main() {
var s1 []int
s2 := []int{}
s3 := make([]int, 0)
fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
// 温馨提示:
// (1)一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量一定都是0;
// (2)我们不能说一个长度和容量都是0的切片一定是nil;
fmt.Printf("s1是否为空: %t\n", s1 == nil)
fmt.Printf("s2是否为空: %t\n", s2 == nil)
fmt.Printf("s3是否为空: %t\n", s3 == nil)
// 综上所述,要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。
fmt.Println(len(s1) == 0)
fmt.Println(len(s2) == 0)
fmt.Println(len(s3) == 0)
}
六.练习题
1.观看代码手写运行结果
package main
import (
"fmt"
)
func main() {
var a = make([]string, 5, 10)
fmt.Printf("数据: %v 长度: %d 容量: %d\n", a, len(a), cap(a))
for i := 0; i < 10; i++ {
a = append(a, fmt.Sprintf("%v", i))
}
fmt.Printf("数据: %v 长度: %d 容量: %d\n", a, len(a), cap(a))
}
2.使用sort包对数组进行排序
package main
import (
"fmt"
"sort"
)
func main() {
var a = [...]int{3, 7, 8, 9, 1}
fmt.Printf("排序前: %v\n", a)
sort.Ints(a[:]) // 对切片进行排序,该切片底层对应的就是上面的可变数组哟~
fmt.Printf("排序后: %v\n", a)
}