您现在的位置是:自如初>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)

文章评论

登录 注册

自如初--时间轴

站名:自如初

独白:向前走!向前走!

邮箱:ziruchu@qq.com

RSS: RSS

站点信息