首页 > 编程语言 > 详细

Go语言 复合数据类型【切片slice】

时间:2021-05-21 18:24:25      阅读:19      评论:0      收藏:0      [点我收藏+]

切片(Slice)

1.1 什么是切片

go语言切片是对数组的抽象。

go数组的长度不可改变,在特定场景中这样的集合就不太适用,go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片是一种方便、灵活且强大的包装器。切片本身没有任何数据。它们只是对现有数组的引用。

切片与数组相比,不需要设定长度,在[]中不用设定值,相对来说比较自由。

从概念上面来说slice像一个结构体,这个结构体包含了三个元素:

  • 1.指针,指向数组中slice指定的开始位置
  • 2.长度,即slice的长度
  • 3.最大长度,也就是slice开始位置到数组的最后位置的长度

1.2 切片的语法

定义切片

var identifier []type

例:

	var s1  []int
	fmt.Println(s1)

	s2 := []int{1,2,3,4}
	fmt.Println(s2)

切片不需要说明长度。

可使用make()函数来创建切片:

var slice1 []type = make([]type, len)
也可以简写为
slice1 := make([]type, len)

make语法

make为slice, map, channel分配内存,并返回一个初始化的值

func make(t Type, size ...IntegerType) Type

第一个参数:类型
      slice,map,chan
第二个参数:长度len
      实际存储元素的数量
第三个参数:容量cap
      最多能够存储的元素的数量

例:创建slice切片

s3 := make([]int,3,8)
fmt.Println(s3)
fmt.Printf("容量:%d,长度:%d\n",cap(s3),len(s3))  //容量:8,长度:3

例:操作切片(重新赋值元素)

s3[0] = 1
s3[1] = 2
s3[2] = 3
fmt.Println(s3)
fmt.Println(s3[3])  //index out of range [3] with length 3

append语法

专门用于向切片的尾部追加元素

语法:func append(slice []Type, elems ...Type) []Type

两种用法:
// 向一个切片的末尾追加元素
slice = append(slice, elem1, elem2)  

//向一个切片的末尾追加另外一个切片的所有元素
slice = append(slice, anotherSlice...)

例:操作切片(向一个切片的末尾追加元素)

//虽然容量为5,但长度为0,实际一个元素都没有
s4 := make([]int,0,5)
fmt.Println(s4)  // 打印:[]


s4 = append(s4,1,2)
fmt.Println(s4) //打印:[1 2]

继续添加元素:
s4 = append(s4,3,4,5,6,7)
fmt.Println(s4) //打印:[1 2 3 4 5 6 7]

注意:虽然已经超出了开始设置的容量,但是slice具有自动扩容的特点,所以可以继续添加元素

例:操作切片(向一个切片的末尾追加另外一个切片的所有元素)

s4 = append(s4,s3...)
fmt.Println(s4) //打印:[1 2 3 4 5 6 7 1 2 3]

遍历切片

遍历某个切片的元素

方法1:
for i := 0;i < len(s4);i++{
		fmt.Println(i)
	}
	
方法2:	
for i,v := range s4{
	fmt.Printf("s4切片下标%d对应的元素为:%d\n",i,v)
}	

1.3 Slice的内存分析以及内存扩容

  • 每一个切片引用了一个底层数组
  • 切片本身不存储任何数据,都是这个底层数组存储,所以修改切片也就是修改这个数组中的数据
  • 当向切片中添加数据时,如果没有超过容量,直接添加;如果超过容量,自动扩容(成倍增长)
  • 切片一旦扩容,就是重新指向一个新的底层数组

代码示例

  • 第一次扩容切片
package main

import (
	"fmt"
)

func  main()  {
	s5 := []int{1,2,3} //创建切片
	fmt.Println(s5)  //打印切片元素内容
	fmt.Printf("长度:%d,容量:%d\n",len(s5),cap(s5)) //打印切片长度及容量
	fmt.Printf("%p\n",s5) //打印切片内存地址
返回值:
[1 2 3]
长度:3,容量:3
0xc00009e140

	s5 = append(s5,4,5)  //扩容切片,追加元素
	fmt.Println(s5) //打印扩容后的切片元素内容
	fmt.Printf("长度:%d,容量:%d\n",len(s5),cap(s5))  ///打印扩容后的切片长度及容量
	fmt.Printf("%p\n",s5) //打印扩容后的切片内存地址
返回值:
[1 2 3 4 5]
长度:5,容量:6
0xc0000c8030
}

注:切片append扩容以后,发生了以下变化:
1、扩容前的容量为3,扩容后的容量成倍增长,变化为6
2、扩容后的长度发生了变化,由3变为5
3、切片一旦扩容,就改变了底层数组,所以底层数组的内存地址发生了改变
  • 第二次扩容切片
继续对上面的切片进行扩容:
	s5 = append(s5,6,7,8)
	fmt.Println(s5)
	fmt.Printf("长度:%d,容量:%d\n",len(s5),cap(s5))
	fmt.Printf("%p\n",s5)
返回值:
[1 2 3 4 5 6 7 8]
长度:8,容量:12
0xc000086060

注:切片再次发生的变化:
1、扩容前的容量为6,扩容后的容量成倍增长,变化为12
2、扩容后的长度发生了变化,由5变为8
3、内存地址发生了改变,所以每当扩容时,都改变了切片使用的底层数组
  • 第三次添加切片元素
继续对上面的切片进行添加元素:
    s5 = append(s5,9,10)
	fmt.Println(s5)
	fmt.Printf("长度:%d,容量:%d\n",len(s5),cap(s5))
	fmt.Printf("%p\n",s5)
返回值:
[1 2 3 4 5 6 7 8 9 10]
长度:10,容量:12
0xc000086060

注:切片再次添加元素后的变化
1、长度发生了变化
2、容量没有发送变化,因为你再次加了两个元素,也没有超过它的容量,则不需要扩容
3、因为切片本次不需要扩容,所以切片使用的底层数组地址也没有发生改变
  • 第四次扩容切片
s5 = append(s5,11,12,13,14,15)
	fmt.Println(s5)
	fmt.Printf("长度:%d,容量:%d\n",len(s5),cap(s5))
	fmt.Printf("%p\n",s5)
返回值:
[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
长度:15,容量:24
0xc000102000

注:切片再次发生的变化:
1、扩容前的容量为12,扩容后的容量成倍增长,变化为24
2、扩容后的长度发生了变化,由10变为15
3、因为切片再次发生了扩容,所以切片使用的底层数组也发送了变化

画图分析

技术分享图片

1.4 在已有数组上直接创建slice

实现语法

	slice := arr[start:end]
	切片中的数据:[start:end]
	arr[:end],从头到end
	arr[start:]从start到末尾
  • 从已有的数组上,直接创建切片,该切片的底层数组就是当前的数组。
  • 长度是从start到end切割的数据量
  • 但是容量从start到数组的末尾

代码示例

  • 已有数组中直接创建切片
a := [10]int{1,2,3,4,5,6,7,8,9,10}
s1 := a[:5] //1-5
s2 := a[3:8] //4-8
s3 := a[5:] //6-10
s4 := a[:] //1-10
fmt.Println("a",a)   //0xc0000a40a0
fmt.Println("s1",s1) //0xc0000a40a0
fmt.Println("s2",s2) //0xc0000a40b8
fmt.Println("s3",s3) //0xc0000a40c8
fmt.Println("s4",s4) //0xc0000a40a0

注:因为容量从start到数组的末尾,所以切片s2,s3的容量发生了变化,底层数组也进行了改变
  • 获取以上数组及切片的长度和容量
fmt.Printf("数组a的长度:%d,容量%d\n",len(a),cap(a))    // 数组a的长度:10,容量10
fmt.Printf("切片s1的长度:%d,容量%d\n",len(s1),cap(s1)) //切片s1的长度:5,容量10
fmt.Printf("切片s2的长度:%d,容量%d\n",len(s2),cap(s2)) //切片s2的长度:5,容量7
fmt.Printf("切片s3的长度:%d,容量%d\n",len(s3),cap(s3)) //切片s3的长度:5,容量5
fmt.Printf("切片s4的长度:%d,容量%d\n",len(s4),cap(s4)) //切片s4的长度:10,容量10

注:
因为:长度是从start到end切割的数据量
所以:切片s1的长度:5,容量10

因为:容量从start到数组的末尾
所以:
切片s2的长度:5,容量7
切片s3的长度:5,容量5
  • 更改底层数组的内容
a[5] = 100       
fmt.Println(a)     //[1 2 3 4 5 100 7 8 9 10]
fmt.Println(s1)    //[1 2 3 4 5]     
fmt.Println(s2)    //[4 5 100 7 8]
fmt.Println(s3)    //[100 7 8 9 10]
fmt.Println(s4)    //[1 2 3 4 5 100 7 8 9 10]

注:
因为s1、s2、s3、s4都指向了同一个底层数组,修改底层数组的元素后,只要被引用的切片中包含了该元素,那么也会改变
s1因为未包含数组a[5]的元素,所以值不变
  • 更改切片的内容
s2[4] = 200
fmt.Println(a)   //[1 2 3 4 5 100 7 200 9 10]
fmt.Println(s1)  //[1 2 3 4 5]
fmt.Println(s2)  //[4 5 100 7 200]
fmt.Println(s3)  //[100 7 200 9 10]
fmt.Println(s4)  //[1 2 3 4 5 100 7 200 9 10]

注:
在操作切片时,其实就是在操作切片对应的底层数组,所以底层数组及使用该底层数组的切片中的元素都会改变
  • 添加切片元素,实现切片扩容
fmt.Println(len(s1),cap(s1))     //5 10
s1 = append(s1,1,1,1,1,1,2,2,2)  
fmt.Println(len(s1),cap(s1))     //13 20

fmt.Println(a)   //[1 2 3 4 5 100 7 200 9 10]
fmt.Println(s1)  //[1 2 3 4 5 1 1 1 1 1 2 2 2]
fmt.Println(s2)  //[4 5 100 7 200]
fmt.Println(s3)  //[100 7 200 9 10]

fmt.Printf("%p\n",&a)  //0xc000014190
fmt.Printf("%p\n",&s1) //0xc0000044a0


注:
当s1所指向的底层数组容量不足时,则会重新创建一个底层数组(先将原数组内容全部拷贝,再进行元素的添加)
后面再去操作s1切片时,则会指向新的底层数组,与数组a、切片s2、s3、s4没有任何关系了

1.5 切片是引用类型

按照数据类型来分:

  • 基本类型:int、float、string、bool、
  • 复合类型:array、slice、map、struct、pointer、function、chan

按照数据特点来分:

  • 值类型:int、float、string、bool、array ...
    传递的是数据副本

  • 引用类型:Slice
    传递的地址,多个变量指向了同一块内存地址

所以:切片是引用类型的数据,存储了底层数组的引用

代码示例

  • 数组:值类型
a1 := [4]int{1,2,3,4}
a2 := a1
fmt.Println(a1,a2)   //[1 2 3 4] [1 2 3 4]
fmt.Printf("a1内存地址:%p\n",&a1)   //a1内存地址:0xc00009e140
fmt.Printf("a2内存地址:%p\n",&a2)  //a2内存地址:0xc00009e160

a1[0] = 100
fmt.Println(a1,a2)   //[100 2 3 4] [1 2 3 4]

注:
a1赋值给a2,是一种值传递,传递的是数据
所以a1 与 a2的内存地址是不一样的。修改a1中的元素内容,与a2是没有任何关系的
  • 切片:引用类型
s1 := []int{1,2,3,4}    
s2 := s1
fmt.Println(s1,s2)      //[1 2 3 4] [1 2 3 4]
fmt.Printf("s1的内存地址:%p\n",s1)  //s1的内存地址:0xc00009e220
fmt.Printf("s2的内存地址:%p\n",s2)  //s2的内存地址:0xc00009e220

s1[0] = 100 
fmt.Println(s1,s2)  //[100 2 3 4] [100 2 3 4]

注:
s1赋值给s2,是一种引用传递,传递的底层数组
所以s1 与 s2的内存地址是一样的。修改s1中的元素内容,s2也会跟着改变

1.6 深拷贝和浅拷贝

  • 深拷贝:拷贝的是数据本身。
    值类型的数据,默认都是深拷贝:array,int,float,string、bool、struct

  • 浅拷贝:拷贝的数据地址。
    导致多个变量指向同一块内存,引用类型的数据,默认都是浅拷贝:slice、map

因为切片是引用类型的数据,直接拷贝的是地址

代码示例

  • 实现切片的深拷贝
s1 := []int{1,2,3,4}
s2 := make([]int,0) //len:0,cap:0
for i:=0;i<len(s1);i++ {
	s2 = append(s2,s1[i])
}
fmt.Println(s1)  //[1 2 3 4]
fmt.Println(s2)  //[1 2 3 4]

s1[0] = 100
fmt.Println(s1)  //[100 2 3 4]
fmt.Println(s2)  //[1 2 3 4]

注:
以上方式通过make方法创建了s2切片,指向了一个新的空数组
以上方式通过for循环对s2切片不断的添加元素,导致s2不断的进行扩容,每当扩容,都是去重新创建了一个数组,直到元素添加完毕;
这个时候再去改变s1的第一个下标元素,对s2是没有任何影响的

copy函数语法

go语言当中,内置的copy函数专用于给copy进行深拷贝的

func copy(dst, src []Type) int

cppy函数一共有两个参数:

  • dist:目标切片
  • src:源切片

内置函数 copy 将元素从源切片复制到目标切片。(一种特殊情况是,它还会将字符串字节复制到字节切片中。) 源切片和目标切片可能会存在重叠。 copy 的返回值是复制的元素个数,该数为 len(src) 和 len(dst) 中的最小值。

代码示例

package main

import "fmt"

func main()  {
	s1 := []int{1,2,3,4}
	s2 := []int{7,8,9}
	copy(s1,s2)  //将s2中的元素,拷贝到s1中
	fmt.Println(s1) //[7 8 9 4]
	fmt.Println(s2) //[7 8 9]
	
	copy(s2,s1)    //将s1中的元素,拷贝到s2中
	fmt.Println(s1)  //[1 2 3 4]
	fmt.Println(s2)  //[1 2 3]

	copy(s2,s1[2:]) //将s1中的下标2元素至末尾拷贝至s2(s2从下标0开始添加)
	fmt.Println(s1)  //[1 2 3 4]
	fmt.Println(s2)  //[3 4 9]

	copy(s2[1:],s1[2:]) //将s1中的下标2元素至末尾拷贝至s2(s2从下标1开始添加)
	fmt.Println(s1)    //[1 2 3 4] 
	fmt.Println(s2)    //[7 3 4]
}

Go语言 复合数据类型【切片slice】

原文:https://www.cnblogs.com/jemooner/p/14794575.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!