您现在的位置是:自如初>Go基础Go基础
26、Go 复合数据类型 - 切片(slice)
温新
2023-08-30 00:02:46
【Go基础】
98人已围观
简介Go 语言中,数组的长度是不能变化的,但是在很多应用场景中,在定义数组时,数组的长度是不知道的,这样就无法满足序列结合的要求了。Go 提供了切片来解决这个问题。
hi,我是温新
切片的概念
Go 语言中,数组的长度是不能变化的,但是在很多应用场景中,在定义数组时,数组的长度是不知道的,这样就无法满足序列结合的要求了。Go 提供了切片
来解决这个问题。
什么是切片
切片是可变长度的序列,序列中的每个元素都是相同类型。换一句话来说,切片是一个动态数组。切片是围绕动态数组的概念来构建的,可以按需自动增长和缩小。切片的动态增长通过内置函数append()
实现。
切片三元素
可以把切片的数据解构理解为一个结构体,这个结构体由三个元素组成:
type Slice struct {
Data unitptr // 指针
Len int // 长度
Cap int // 最大长度(容量)
}
指针。指向数组中切片指定的开始位置;
长度。即切片的长度;
容量。切片开始位置到数组最后位置的长度。
切片的语法
声明切片
1、声明空切片
声明一个未指定长度的数组来定义切片,语法如下:
// 格式
var 变量名 []类型
声明切片时不用指定长度,采用该声明方式且未初始化的切片为空切片
。该切片默认为 nil,长度为 0。nil 切片在底层数组中包含 0 个元素,也没有分配任何存储空间。
package main
import "fmt"
func main() {
// 声明 nil 整型切片
var sli []int
fmt.Println(sli) // []
}
2、make 函数创建切片
使用 make()
函数创建切片的语法:
// 格式
var slice []type = make([]type, len)
简写方式如下:
// 格式
slice := make([]type, len)
创建切片时可以指定容量,其中cap
为可选参数,用于指定切片容量:make([]类型,长度,容量)
。当不指定容量时,则容量默认与长度相同。案例如下:
package main
import "fmt"
func main() {
// 方式一:完整声明方式
var sli []int = make([]int, 3)
fmt.Println(sli) //[0 0 0]
// 方式二:简写方式
sli1 := make([]int, 3)
fmt.Println(sli1) // [0 0 0]
fmt.Printf("长度=%d 容量=%d slice=%v\n", len(sli1), cap(sli1), sli1)
}
输出结果
[0 0 0]
[0 0 0]
长度=3 容量=3 slice=[0 0 0]
切片初始化
1、通过字面量初始化
package main
import "fmt"
func main() {
sli := []int{1, 2, 3, 4}
fmt.Println(sli)
}
2、截取数组初始化切片
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
// [起始位置:结束位置] 截取数组
sli := arr[1:3]
fmt.Println("切片", sli)
fmt.Printf("数组类型 %T\n", arr)
fmt.Printf("切片类型 %T\n", sli)
}
输出结果
切片 [2 3]
数组类型 [5]int
切片类型 []int
1、通过截取数组进行切片时,如[1:3]
,从数组下标 1 的位置开始,到 3 的位置结束,但是不包括 3;
2、数组类型有明确的长度;
3、切片没有长度;
4、总结:有固定长度的是数组,没有长度的是切片。
3、截取切片来初始化切片
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7}
// 切片中进行切片
newSlice := numbers[0:5]
printSlice(newSlice)
// 从截取中的切片再进行切片
printSlice(newSlice[1:4])
}
func printSlice(s []int) {
fmt.Printf("长度=%d 容量=%d slice=%v\n", len(s), cap(s), s)
}
输出结果
长度=5 容量=7 slice=[1 2 3 4 5]
长度=3 容量=6 slice=[2 3 4]
len 和 cap 函数
切片的长度是切片中元素的数量。
切片的容量是从创建切片的索引开始的底层数组中元素的数量。
切片可以通过 len()
方法获取长度,可以通过 cap()
方法获取容量。
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7}
fmt.Printf("长度=%d 容量=%d slice=%v\n", len(numbers), cap(numbers), numbers)
}
切片是引用类型
切片是对数组底层一个引用,它没有自己的类型。对切片做的所有修改都将反映在底层数组中。
数组是一个值类型,而切片是一个引用类型。
package main
import "fmt"
func main() {
num := [5]int{1, 2, 3, 4, 5}
// 对数组进行切片
sli := num[0:3]
// 修改切片中的元素的值
sli[0] = 100
fmt.Println(num)
fmt.Println(sli)
}
输出结果
[100 2 3 4 5]
[100 2 3]
结论:在切片中修改元素的值,实际是在修改数组元素的值,因此彼此受到影响。
切片的使用
切片赋值
对切片的某个元素赋值在方法上与数组一致,使用[]
符号。
package main
import "fmt"
func main() {
sli := []int{1, 2, 3, 4, 5}
// 改变索引为 0 的元素的值
sli[0] = 100
fmt.Println(sli)
}
切片分割
切片分割,其实就是把底层数组切出一部分。如底层数组元素有 [a,b,c,d,e]
,切片就从这些元素中切出一部分。
package main
import "fmt"
func main() {
sli := []int{1, 2, 3, 4, 56}
// 切片
newSlice1 := sli[1:3]
// 从下标 2 开始截取,截取到 3,切片容量为 2l
newSlice2 := sli[2:3:4]
fmt.Println(newSlice1)
fmt.Println(newSlice2)
}
输出结果
长度=2 容量=4 newSlice1=[2 3]
长度=1 容量=2 newSlice2=[3]
上面的案例中,对 sli 进行了两次切片,它们都共享一段底层数组,但不同切片看到的底层数组不一样。
下面看看sli[2:3:4]
,其规则是sli[low,high,max]
:
1、low:下标起点(切片中第一个元素在原数组中的索引下标);
2、high:下标的终点(不包括此下标);
3、len = high - low(长度:当前切片所存放的元素个数);
4、cap = max - low(容量:切点在不扩容的情况下最多能存放的元素的个数)
看到这里,可以这样去理解计算,切片的容量就是在原数组中从切片的第一个元素开始,到原数组最后一个元素的元素个数。如sli[2:3:4]
,其 cap = 4 - 2,其 len = 3 - 2。
用作公式来套路的话,sli[low,high,max]
,其底层数组容量 max 的来说,其长度为 high - low
,容量为 max - low
切片扩容
切片扩展使用内置函数 append()
。append
用于往切片中追加新元素,可以追加一个或多个元素,也可以追加一个切片。
append()
会改变切片所引用的数组的内容,从而影响到引用同一数组的其他切片。
当使用 append()
追加元素到切片时,若容量不够(即 (cap - len) == 0),Go 就会创建一个新的内存地址来存储元素。
package main
import "fmt"
func main() {
sli := []int{1, 2, 3, 4, 5}
// 创建一个新切片,其长度为 2,容量为 4
newSlice := sli[1:3]
newSlice = append(newSlice, 6)
fmt.Println(newSlice)
fmt.Printf("%d, %d\n", len(newSlice), cap(newSlice))
// 创建一个切片
sli1 := []int{10, 20, 30, 40}
// 容量不够时,会自动扩容
newSlice1 := append(sli1, 50)
fmt.Printf("%d, %d\n", len(newSlice1), cap(newSlice1))
}
遍历切片
切片是一个集合,可以使用 range 来迭代元素。
package main
import "fmt"
func main() {
names := []string{"西施", "王昭君", "貂蝉", "杨贵妃"}
// 使用 for...range 迭代
for index, name := range names {
fmt.Printf("编号: %d, 名字: %s\n", index, name)
}
// 使用 for 循环
for i := 0; i < len(names); i++ {
fmt.Printf("编号: %d, 名字: %s\n", i, names[i])
}
}
限制容量
创建切片时,还有第三个可选参数,用于限制新切片的容量。其目的不是要增加容量,而是限制容量。
package main
import "fmt"
func main() {
// 创建字符串切片
// 其长度和容量都是 5 个元素
fruits := []string{"apple", "orange", "plum", "banana", "grape"}
// 切片出第三个元素,并限制容量
// 其长度为 1 个元素,容量为 2 个元素
sli := fruits[2:3:4]
fmt.Printf("长度=%d 容量=%d sli=%v\n", len(sli), cap(sli), sli)
}
程序执行后,新切片 sli
会从底层数组中引用 1 个元素,容量是 2 个元素。引用的元素是 plum
,并将容量扩展到了 banana
元素。
利用之前的计算方式来算一算,sli[low, high, max](2:3:4)
,长度为 high - low 即(3 - 2 = 1)
,容量为 max - low 即 (4 - 2 = 2 )
。
数组是共享内存的,使用切片时,稍不注意就可能因为切片正在共享一个底层数组而出现问题。而对切片内容的修改以会影响到多个切片。若在创建切片时设置切片的容量和长度一样,就可能强制让新切片的第一个 append()
操作创建新的底层数组,从而与原有的底层数组隔离,隔离后就可以安全的进行修改。
package main
import "fmt"
func main() {
// 创建字符串切片
// 其长度和容量都是 5 个元素
fruits := []string{"apple", "orange", "plum", "banana", "grape"}
// 切片出第三个元素,并限制容量
// 其长度为 1 个元素,容量为 2 个元素
sli := fruits[2:3:4]
fmt.Printf("长度=%d 容量=%d sli=%v\n", len(sli), cap(sli), sli)
// 限制容量
newSli := fruits[2:3:3]
newSli = append(newSli, "kiwi")
fmt.Printf("长度=%d 容量=%d sli=%v\n", len(newSli), cap(newSli), newSli)
fmt.Printf("长度=%d 容量=%d sli=%v\n", len(fruits), cap(fruits), fruits)
}
多维切片
多维切片的形式如下:
// 创建一个整型切片的切片
sli := [][]int{
{10},
{100, 200},
}
这个示例中,外层是一个切片,内层又是一个切片,就组成了多维切片。多维切片追加还是使用 append
。
package main
import "fmt"
func main() {
sli := [][]int{
{10},
{100, 200},
}
// 追加元素
sli[0] = append(sli[0], 20)
fmt.Println(sli) // [[10 20] [100 200]]
}
切片传递给函数
在函数之间传递切片就是要在函数间以值的方式传递切片。由于切片尺寸小,在函数间复制和传递的成本很低。
package main
import "fmt"
func main() {
nums := []int{1, 2, 3}
sli := foo(nums)
fmt.Println(sli)
}
func foo(sli []int) []int {
return sli
}
很赞哦!(4)