——引言:无论是C语言中的数组还是Go语言中的数组,数组的长度一旦确定就不能改变, 但在实际开发中我们可能事先不能确定数组的长度, 为了解决这类问题Go语言中推出了一种新的数据类型切片
-
什么是切片?
1.切片简单理解就是一个可变长度的数组底层的实现原理就是一个结构体, 结构体中有一个指针指向了一个数组
2.本质上所有的数据都是保存在指向的数组中的
先看一遍这个图,留个印象,一会儿再看一遍
emmmmm,再看下底层源码
//切片源码
type slice struct{
array unsafe.Pointer // 指向底层数组指针
len int // 切片长度(保存了多少个元素)
cap int // 切片容量(可以保存多少个元素)
}
-
如何创建切片
1.通过数组来创建
格式: [起始位置:结束位置], 从起始位置开始截取, 直到结束位置, 但是不包括结束位置
注意: 截取了多少个元素, len就等于几
容量等于数组的长度 - 起始位置
package main
import "fmt"
func main() {
// 1.通过数组来创建
// 1.创建一个数组
// 0 1 2 3 4
var ages [5]int = [5]int{1, 3, 5, 7, 9}
// 2.通过数组创建切片
// [起始位置:结束位置], 从起始位置开始截取, 直到结束位置, 但是不包括结束位置
var sce01 []int = ages[0:2]
var sce02 []int = ages[2:4]
fmt.Println(sce01) //[1 3]
fmt.Println(sce02) //[5 7]
// 注意点: 如果只写了起始位置, 没有写结束位置, 那么会截取到最后
var sce03 []int = ages[0:]
var sce04 []int = ages[2:]
fmt.Println(sce03) //[1 3 5 7 9]
fmt.Println(sce04) //[5 7 9]
// 注意点: 如果只写了结束位置, 没有写开始位置, 那么会从开始截取到指定的结束位置
var sce05 []int = ages[:2]
fmt.Println(sce05) //[1 3]
// 注意点: 如果只写了:, 那么就是从开始截取到末尾
var sce06 []int = ages[:]
fmt.Println(sce06) //[1 3 5 7 9]
// 计算切片当前保存数据的个数
fmt.Println(len(sce06)) //5
// 计算切片总共可以保存数据的个数
fmt.Println(cap(sce06)) //5
var ages1 [5]int = [5]int{1,3,5,7,8}
var sce1 []int = ages1[2:4]
var sce2 []int = ages1[0:]
var sce3 []int = ages1[2:]
var sce4 []int = ages1[:4]
fmt.Println(sce1)
fmt.Println(sce2)
fmt.Println(sce3)
fmt.Println(sce4)
fmt.Println(len(sce4))
fmt.Println(cap(sce4))
}
2.通过make函数来创建
- 格式: make([]数据类型, 长度, 容量)
- 内部会先创建一个数组,让切片指向数组
- 不指定容量时切片容量等于长度
// 2.通过make函数创建切片
// 第一个参数: 告诉系统要存储什么类型的数据
// 注意点: 如果是创建切片一定要在传入的数据类型前面写上[]
// 第二个参数: 告诉系统创建出来的切片len等于多少
// 第三个参数: 告诉系统创建出来的切片cap等于多少
// 注意点: 第三个参数可以省略, 如果省略切片的容量就等于切片的长度(等于第二个参数)
var sce []int = make([]int, 2, 5)
//var sce []int = make([]int, 2) //省略时默认容量cap=len,即2
fmt.Println(sce) //[0 0],初始化为0值
fmt.Println(len(sce)) //2
fmt.Println(cap(sce)) //5
3.通过Go提供的语法糖来创建
// 3.通过Go提供的语法糖来创建
// 一定要注意[]里面没有值就是切片
// 通过Go提供的语法糖来创建len等于cap
var sce []int = []int{1, 3, 5} // 相当于make([]int, 3)
fmt.Println(sce) //[1 3 5]
fmt.Println(len(sce)) //3
fmt.Println(cap(sce)) //3
var sce6 []int = []int{2,4,6}
fmt.Println(sce6) //[2 4 6]
fmt.Println(len(sce6)) //3
fmt.Println(cap(sce6)) //3
sce7:=[]int{1,1,1}
fmt.Println(sce7) //[1 1 1]
切片和数组
- 切片和数组最直观的区别
[]中没有数字就是切片, []中有数字就是数组(或者省略格式“...”) - 数组:
// 数组
var ages1 [3]int = [3]int{1, 3, 5} // 这里是数组
var ages2 = [3]int{1, 3, 5} // 这里是数组
ages3 := [3]int{1, 3, 5} // 这里是数组
ages4 := [...]int{1, 3, 5} // 这里写...代表是定义一个数组
fmt.Println(ages1,ages2,ages3,ages4)
- 切片:
// 2.切片
var ages5 []int = []int{1, 3, 5} // 省略元素个数, 这里不是数组是切片
var ages6 = []int{1, 3, 5} // 省略元素个数, 这里不是数组是切片
ages7 := []int{1, 3, 5} // 省略元素个数, 这里不是数组是切片
fmt.Println(ages5,ages6,ages7)
- 二维数组
// 二维数组
// 定义一个数组, 数组中存储的又是数组
var values1 [2][3]int = [2][3]int{
{1, 3, 5},
{2, 4, 5},
}
var values2 = [2][3]int{
{1, 3, 5},
{2, 4, 5},
}
values3 := [2][3]int{
{1, 3, 5},
{2, 4, 5},
}
values4 := [...][3]int{
{1, 3, 5},
{2, 4, 5},
}
fmt.Println(values1,values2,values3,values4)
- 切片存数组
// 切片
// 定义一个切片, 切片中保存的是数组
var values01 [][3]int = [][3]int{
{1, 3, 5},
{2, 4, 5},
}
var values02 = [][3]int{
{1, 3, 5},
{2, 4, 5},
}
values03 := [][3]int{
{1, 3, 5},
{2, 4, 5},
}
fmt.Println(values01,values02,values03)
- 切片存切片
// 定义一个切片, 切片中保存的是切片
var values11 [][]int = [][]int{
{1, 3, 5},
{2, 4, 5},
}
var values12 = [][]int{
{1, 3, 5},
{2, 4, 5},
}
values13 := [][]int{
{1, 3, 5},
{2, 4, 5},
}
fmt.Println(values11,values12,values13)
- 注意点:
只要数组的[]中没有编写数字, 那么这个就不是一个数组, 这就是一个切片
切片的基本使用
- 1.可以像使用普通数组那样使用切片
var sce []int = make([]int, 2, 5)
fmt.Println(sce)
sce[0] = 1
sce[1] = 2
// 注意点: 如果像使用数组一样使用切片, 那么索引的长度不能超过切片的len
//sce[2] = 999 //报错 超过len
2.可以通过预先定义好的函数来使用切片
append(切片, 数据)3.append函数的作用:
在len后面追加数据
var sce []int = make([]int, 2, 5)
fmt.Println(sce)
sce[0] = 1
sce[1] = 2
//用append函数可以在len后面追加切片的元素
sce = append(sce, 3)
fmt.Println(sce) //[1 2 3]
fmt.Printf("%p\n", sce) //0xc04206a0c0,切片是地址传递,结构体中有指针指向数组
sce = append(sce, 4)
fmt.Printf("%p\n", sce) //0xc04206a0c0
sce = append(sce, 5)
fmt.Printf("%p\n", sce) //0xc04206a0c0
fmt.Println(sce) //[1 2 3 4 5]
fmt.Println(cap(sce)) //5
// 超过容量
sce = append(sce, 6)
fmt.Printf("%p\n", sce) //0xc042088000 超过原本容量会重新分配内存空间,地址会变
fmt.Println(sce) //[1 2 3 4 5 6]
fmt.Println(cap(sce)) //10 会扩容成原本容量的两倍
- 4.容量
如果通过append函数追加数据之后超过了原有的容量, 那么系统内部会自动按照当前容量*2的方式重新定义一个数组作为切片保存数据的模型
var sce []int = make([]int, 1024, 1024)
fmt.Printf("%p\n", sce) //0xc042072000
fmt.Println(cap(sce)) //1024
sce = append(sce, 1025)
fmt.Printf("%p\n", sce) //0xc042078000
fmt.Println(cap(sce)) //1280
- 5.为什么append函数要返回一个切片
因为在扩容的时候会重新定义一个新的切片, 所以需要返回一个切片
切片的高级用法(假装很高级)
- 切片的增删查改
// 1.定义一个切片
sce := []int{1, 3, 5, 7, 9}
// 2.修改切片中的值
// 切片名称[索引] = 值
fmt.Println(sce)
sce[1] = 666
fmt.Println(sce)
// 3.给切片增加数据
fmt.Println(sce)
sce = append(sce, 666)
fmt.Println(sce)
// 4.查询切片中的数据
for _, value := range sce {
if value == 3 {
fmt.Println("包含数字3")
}
}
//// 5.删除切片中指定的元素
// {1, 3, 5, 7, 9}
index := 2 // 需要删除元素的索引
//注意点: 不仅能够通过数组生成切片, 还可以通过切片生成切片
//sce = sce[:index] // {1, 3}
//fmt.Println(sce)
//sce = sce[index + 1:] //{7, 9}
//fmt.Println(sce)
//发现规律: 将第二次截取的切片添加到第一次截取的切片后面即可
//{1, 3, 7, 9}
//...是可变参数
sce = append(sce[:index], sce[index + 1:]...)
fmt.Println(sce)
- 通过数组生成切片和切片生成切片的步骤及注意点(关注地址问题)
// 1.创建一个数组
var arr [5]int = [5]int{1, 3, 5, 7, 9} // 数组
// Go语言中不能直接通过数组名获取数组的地址,与C语言不同
//fmt.Printf("%p\n", arr) //不能这样写,必须有取地址符&
fmt.Printf("%p\n", &arr)// 0xc04206a0f0
fmt.Printf("%p\n", &arr[0])//0xc04206a0f0
// 2.通过数组创建一个切片
var sce0 []int = arr[:]
// 直接打印sce0打印的是sce中指针保存的地址
// 也就是底层指向的那个数组的地址
fmt.Printf("%p\n", sce0) //0xc04206a0f0
//打印&sce0才是切片自己的地址
fmt.Printf("%p\n", &sce0) //0xc0420504a0
// 3.通过切片创建一个切片
// 通过切片创建一个切片, 新的切片和老的切片底层指向同一个数组
var sce2 []int = sce0[:]
fmt.Printf("%p\n", sce0) //0xc04206a0f0
// 4.会直接修改指向的同一个数组的值
//arr[1] = 666
//sce[1] = 666
sce2[1] = 666
fmt.Println(arr)
fmt.Println(sce)
fmt.Println(sce2)
注意点:切片可以再生成新的切片, 两个切片底层指向同一个数组
现在再看一遍这个图,是不是醍醐灌顶
- 通过数组生成切片补充
- 格式: 数组名称[low: high, max]
// 0 1 2 3 4 var arr [5]int = [5]int{1, 3, 5, 7, 9} //不指定切片长度 var sce0 []int = arr[2:4] fmt.Println(sce0) // [5 7] fmt.Println(len(sce0)) // 2 high-low = 切片长度 fmt.Println(cap(sce0)) // 3 数组以前的长度 - 截取的开始位置 = 容量 // 总结: 如果没有指定第三个参数, 那么切片的容量就等于 数组的容量 - 第一个参数 // 如果指定了第三个参数, 那么切片的容量就等于 第三个参数 - 第一个参数 // 切片的第三个参数max,必须大于或等于第二个参数且不能超过数组的长度(越界) //指定切片长度 var sce []int = arr[2:4:5] fmt.Println(sce) fmt.Println(len(sce)) fmt.Println(cap(sce))
- 仅仅定义, 没有初始化的数组是可以使用的, 但是仅仅定义但是没有初始化的切片是不能使用的(零值问题)
// 1.定义一个数组变量 var arr [3]int // 2.使用数组变量 //arr[0] = 1 arr[1] = 3 arr[2] = 5 // 3.打印使用之后的结果 fmt.Println(arr) //[0 3 5] 数组会自动给元素赋零值 //1.定义一个切片变量 var sce []int //对切片进行初始化 sce = make([]int, 3, 5) //会内建数组,所以也会初始化为零值 //2.使用切片变量 //赋值 sce[0] = 1 sce[1] = 3 sce[2] = 5 //追加 sce = append(sce, 2) sce = append(sce, 4) sce = append(sce, 6) // 3.打印使用之后的结果 fmt.Println(sce) //[1 3 5 2 4 6] fmt.Println(cap(sce)) //10 append自动扩容两倍
- 计算切片长度(与数组对比)
var arr [3]int = [3]int{1, 3, 5} // 第一种方式: 推荐方式 length := len(arr) fmt.Println(length) // 第二种方式: 不推荐方式 size := unsafe.Sizeof(arr) fmt.Println(size) eleSize := unsafe.Sizeof(arr[0]) fmt.Println(eleSize) fmt.Println(size/eleSize) var sce []int = []int{1, 3, 5, 7, 9} // 方式一: 推荐方式 length2 := len(sce) fmt.Println(length2) // 方式二: 注意点, 切片不能通过sizeof来计算 // 由于切片是一个结构体内部指向了一个数组, 所以通过sizeof计算的是切片本身结构体的大小, 而不是切片的长度 //size := unsafe.Sizeof(sce) //fmt.Println(size)
可变参数
可变参数底层其实就是一个切片
使用数组传递参数时必须明确参数长度
package main
import "fmt"
func main() {
arr := [3]int{10, 20, 30}
res := sum2(arr)
fmt.Println(res)
sce := []int{10, 20, 30}
result := sum1(sce)
fmt.Println(result)
}
func sum2(nums [3]int) int{
// 1.获取数组的长度
length := len(nums)
var res int // 保存计算结果
// 2.遍历数组
for i := 0; i < length; i++ {
res += nums[i]
}
return res
}
func sum1(nums []int) int{
// 2.遍历切片
var res int
for _, value := range nums{
res += value
}
return res
}
- 使用可变参数(切片)时不需要对参数长度进行计算
package main
import "fmt"
func main() {
res:=sum( 10, 20, 30)
fmt.Println(res)
res2:=sum02(3.14, 10, 20, 30)
fmt.Println(res2)
}
// 注意点: 如果函数编写了可变参数, 那么可变参数只能放在形参列表的最后
func sum( nums ...int) int{
var res int
for _,value := range nums {
res += value
}
return res
}
func sum02(value float32, nums ...int) int{
var res int
for _,value := range nums {
res += value
}
return res
}
- 对照两段代码,再次验证了可变参数的底层实现实际上是切片
copy函数
package main
import "fmt"
func main() {
sce1 := []int{1, 3, 4} // 源切片
sce12 := []int{1, 3, 4, 5} // 源切片
sce13 := []int{1, 3} // 源切片
sce2 := []int{0, 0, 0} // 目标切片
sce02 := []int{0, 0, 0} // 目标切片
sce03 := []int{0, 0, 0} // 目标切片
// 第一个参数:目标切片
// 第二个参数:源切片
// 会将源切片中的数据拷贝到目标切片中
// 拷贝的时候会以目标切片为主(源切片元素更多则舍去,元素更少则补0)
copy(sce2, sce1)
copy(sce02, sce12)
copy(sce03, sce13)
fmt.Println(sce2) //[1 3 4]
fmt.Println(sce02) //[1 3 4]
fmt.Println(sce03) //[1 3 0]
}
切片补充
- 切片和数组不同, 切片不支持== !=操作
var arr1 [3]int = [3]int{1, 3, 5}
var arr2 [3]int = [3]int{1, 3, 5}
res1 := arr1 == arr2
fmt.Println(res1) //true
var sce1 []int = []int{1, 3, 5}
var sce2 []int = []int{1, 3, 5}
res2 := sce1 == sce2 //会报错,切记切片是地址拷贝
fmt.Println(res2)
- 在Go语言中字符串的底层实现就是切片, 所以可以通过字符串来生成切片
var str string = "www.doudou.com"
//str[0] = 'M' //这样操作会报错,不可以直接修改字符串
//sce := str[:]
sce := str[4:]
fmt.Printf("%T\n", sce) //string 此切片的数据类型
fmt.Println(sce) //doudou.com
var str2 string = "www.doudou.com"
sce2 := make([]byte, 3) //byte用于存储一个字符
copy(sce2, str2)
fmt.Println(sce2) //[119 119 119] 输出ASCLL码
fmt.Printf("%c\n", sce2[0]) //w
fmt.Printf("%c\n", sce2[1]) //w
fmt.Printf("%c\n", sce2[2]) //w
sce3 := make([]byte, len(str2))
sce0 :=make([]byte,len(str2))
copy(sce3, str2)
copy(sce0, str2)
sce3[0] ='M'
sce0[0] ='N'
//用切片可以间接修改字符串
fmt.Printf("%s\n", sce3) //Mww.doudou.com
fmt.Printf("%s\n", sce0) //Nww.doudou.com
数组和切片做函数参数的区别(值传递和地址传递)
- 数组作为函数的参数, 是值传递, 在函数内修改形参, 不会影响函数外的实参
- 切片作为函数的参数, 是地址传递(指针), 在函数内修改形参, 会影响到函数外的实参
func main() {
var arr [3]int = [3]int{1, 3, 5}
fmt.Println(arr) //[1 3 5]
change1(arr)
fmt.Println(arr) //[1 3 5]
var sce []int = []int{1, 3, 5}
fmt.Println(sce) //[1 3 5]
change2(sce)
fmt.Println(sce) //[1 666 5]
}
func change2(nums []int) {
nums[1] = 666
}
func change1(nums [3]int) {
nums[1] = 666
}
有疑问加站长微信联系(非本文作者)