【Go学习笔记】数据类型之切片(slice)

3 篇文章 0 订阅

切片 是Go中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。它非常灵活,支持自动扩容。切片的底层一段连续的内存。

切片的内部实现

切片是一个有三个字段的数据结构,分别为地址长度容量,它对底层的数组(内部是通过数组保存数据的)进行了抽象,并提供相关的操作方法。

切片与底层数组的对应关系
地址:指向底层数组的指针。
长度:切片可以访问的元素的个数,使用内置函数len(切片名)可以获得。
容量:从切片地址开始到底层数组结尾的长度,使用内置函数cap(切片名)可以获得。

切片的创建和初始化

在Golang中可以通过多种方式创建和初始化切片。可以根据切片所需的容量来决定如何创建切片。

切片的声明

切片的声明格式如下:

	var 切片名 []元素类型
	
	例如,声明一个地址为nil的整型切片:
	var myNum []int   // 例:声明一个地址为nil的整型切片

单纯声明后的切片并没有分配内存空间,因此地址指向nil,可以称之为nil切片。nil切片的长度和容量都为0,数据结构状态如下:
nil切片的数据结构状态
在Golang中,nil切片很常见,可以使用很多标准库和内置函数。在需要描述一个不存在的切片时,nil切片会很好用。比如,函数要求返回一个切片但是发生异常的时候。

通过make()函数创建切片

使用Golang内置的make()函数,动态创建一个切片,格式如下:

    make([]类型, 长度, 容量)
    
    a := make([]int, 3, 5)   // 定义一个长度为3,容量为5,元素为int类型的切片
    fmt.Println(a)           // [0 0 0] 切片可访问的元素数为3
    fmt.Println(len(a))      // 3
	fmt.Println(cap(a))      // 5
	fmt.Printf("%#v \n", a)  // []int{0, 0, 0}

示例代码中a的内部存储空间已经分配了5个,但实际只是用了3个。容量并不会影响当前元素的个数。

当要创建的切片长度=容量时,可以使用以下格式进行创建:

	make([]类型, 长度)         // 创建长度=容量的切片  
	
    b := make([]int, 3)      // 定义一个长度为3,容量为3,元素为int类型的切片
    fmt.Println(a)           // [0 0 0]
    fmt.Println(len(a))      // 3
	fmt.Println(cap(a))      // 3
	fmt.Printf("%#v \n", a)  // []int{0, 0, 0}

创建切片时,长度和容量是可以为0的,此时地址指针不为nil,称为空切片。下图描述了空切片的状态。
空切片的数据结构状态

通过字面量创建切片

另一种常用的创建切片的方法是使用切片字面量,这种方法和创建数组类似,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定:

	// 创建字符串切片
	// 其长度和容量都是3个元素
	myStr := []string{"Jack", "Mark", "Nick"}
	fmt.Printf("myStr = %#v,长度=%d,容量=%d \n", myStr, len(myStr), cap(myStr))
	// output: myStr = []string{"Jack", "Mark", "Nick"},长度=3,容量=3
	
	// 创建一个整型切片
	// 其长度和容量都是4个元素
	myNum := []int{10, 20, 30, 40}
	fmt.Printf("%#v,长度=%d,容量=%d \n", myNum, len(myNum), cap(myNum))
	// output: []int{10, 20, 30, 40},长度=4,容量=4

当使用切片字面量创建切片时,还可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。下面的语法展示了如何使用索引方式创建长度和容量都是100个元素的切片:

	// 创建字符串切片
	// 使用空字符串初始化第 100 个元素
	myStr := []string{99: ""}

通过切片/数组创建切片

切片的本质是底层数组切出的一部分,因此可以通过字符串/数组/指向数组或切片的指针构造新切片。
它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外,还指定容量的完整的形式。
创建新切片的语法如下:

    slice[i:j]   (array[i:j])
    slice[i:j:k] (array[i:j:k])

i: 表示从 slice/array 的第几个元素开始切
j: 控制切片的长度(j-i),一般不包含索引为j的元素。
k: 控制切片的容量(k-i),如果没有给定 k,则表示切到底层数组的最尾部。
下面是几种常见的简写形式:

	slice[i:]  // 从 i 切到最尾部
	slice[:j]  // 从最开头(0)切到 j(不包含j)
	slice[:]   // 从头切到尾,等价于复制整个slice

让我们通过下面的例子来理解通过切片创建新的切片的本质:

	// 通过字面量创建一个整型切片
    // 其长度和容量都是 5 个元素
    myNum := []int{10, 20, 30, 40, 50}
    
    // 创建一个基于myNum 的新切片
    // i = 1, j = 3,k未指定,切到最尾部
    // 长度 = j-i = 2 个元素,容量为 4 个元素
    newNum := myNum[1:3]
    fmt.Printf("newNum=%v,长度=%d,容量=%d \n", newNum, len(newNum), cap(newNum))
    // output: newNum=[20 30],长度=2,容量=4

执行上面的代码后,我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分:
指向同一底层数组的不同切片注意:截取新切片时的原则是 左含右不含。所以 newNum 是从 myNum 的 index=1 处开始截取,截取到 index=3 的前一个元素,也就是不包含 index=3 这个元素。所以,新的 newNum 是由 myNum 中的第2个元素、第3个元素组成,长度为 2,容量为 4。切片 myNum 能够看到底层数组全部 5 个元素的容量,而 newNum 能看到的底层数组的容量只有 4 个元素。newNum 无法访问到底层数组的第一个元素。所以,对 newNum 来说,那个元素就是不存在的。

对切片的操作

为切片添加元素(append())

Go语言的内置函数append()可以为切片动态添加元素。一次可以添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。

	var s []int
	// 添加一个元素
	s = append(s, 1)       // [1]
	// 添加多个元素
	s = append(s, 2, 3, 4) // [1 2 3 4]
	s2 := []int{5, 6, 7}
	// 添加另一个切片的所有元素
	s = append(s, s2...)   // [1 2 3 4 5 6 7]

注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。

切片扩容

相对于数组而言,使用切片的一个好处是:可以按需增加切片的容量。Golang 内置的 append() 函数会处理增加长度时的所有操作细节。要使用 append() 函数,需要一个被操作的切片和一个要追加的值,当 append() 函数返回时,会返回一个包含修改结果的新切片。函数 append() 总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量。

例如:

	func main() {
		//append()添加元素和切片扩容
		var numSlice []int
		for i := 0; i < 10; i++ {
			numSlice = append(numSlice, i)
			fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
		}
	}

输出结果:

	[0] len:1 cap:1 ptr:0xc000014098
	[0 1] len:2 cap:2 ptr:0xc0000140e0
	[0 1 2] len:3 cap:4 ptr:0xc0000121e0
	[0 1 2 3] len:4 cap:4 ptr:0xc0000121e0
	[0 1 2 3 4] len:5 cap:8 ptr:0xc00000e340
	[0 1 2 3 4 5] len:6 cap:8 ptr:0xc00000e340
	[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc00000e340
	[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc00000e340
	[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc000018100
	[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc000018100

从上面的结果可以看出:

  1. append()函数将元素追加到切片的最后,并返回该切片。
  2. 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。

函数 append() 会智能地处理底层数组的容量增长。在切片的容量小于 1024 个元素时,总是会成倍地增加容量。一旦元素个数超过 1024,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量(随着语言的演化,这种增长算法可能会有所改变)。
想要了解切片详细的扩容策略,可以查看$GOROOT/src/runtime/slice.go的源代码。

删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:

	func main() {
		// 从切片中删除元素
		a := []int{30, 31, 32, 33, 34, 35, 36, 37}
		// 要删除索引为2的元素(32)
		a = append(a[:2], a[3:]...)
		fmt.Println(a) //[30 31 33 34 35 36 37]
	}

总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

遍历切片

切片的遍历方式和数组是一致的,支持索引遍历for range遍历

func main() {
	s := []int{1, 3, 5}

	// 索引遍历
	for i := 0; i < len(s); i++ {
		fmt.Println(i, s[i])
	}

	// for range遍历
	for index, value := range s {
		fmt.Println(index, value)
		// value += 1  只能修改副本值,无法修改切片元素内容
		s[index] += 1  //可以修改切片元素内容
	}
}

需要注意的是,for range创建了每个元素的副本,而不是直接返回对该元素的引用。要想获取每个元素的地址,可以使用切片变量和索引值。

修改切片元素

使用切片变量和索引值修改切片元素的内容。

切片的复制

Golang 内置的 copy() 函数可以将一个切片中的元素拷贝到另一个切片中,其函数声明为:

func copy(dst, src []Type) int

它表示把切片 src 中的元素拷贝到切片 dst 中,返回值为拷贝成功的元素个数。如果 src 比 dst 长,就截断;如果 src 比 dst 短,则只拷贝 src 那部分:

num1 := []int{10, 20, 30}
num2 := make([]int, 5)
count := copy(num2, num1)
fmt.Println(count)
fmt.Println(num2)

运行这段单面,输出的结果为:

3
[10 20 30 0 0]

3 表示拷贝成功的元素个数。

参考资料

(李文周)Go语言基础之切片
Golang 入门 : 切片(slice)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值