09.Go切片

Go语言提供了另一种数据类型——切片(Slice),由于切片的数据结构中有指向数组的指针,因此它是一种引用类型。Go语言中切片的内部结构包含地址、大小和容量,切片一般用于快速操作数据集合。

切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数append()来实现的,这个函数可以快速且高效地增长切片,也可以通过对切片再次切割,缩小一个切片的大小。因为切片的底层内存也是在连续内存块中分配的,所以,切片还能获得索引、迭代及为垃圾回收优化的好处。

1、切片的创建

在Go语言中,创建切片的方法有多种,而能否确定切片的容量是创建切片的关键,它决定了该使用哪种方式创建切片。

a. 从数组或切片生成新的切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。从连续内存区域生成切片是常见的操作。

格式:slice [开始位置 : 结束位置]

  • slice表示目标切片对象。
  • 开始位置表示对应目标切片对象的索引。
  • 结束位置表示对应目标切片的结束索引。
// 从数组中生成切片
var a = [3]int{1, 2, 3}
fmt.Println(a)			// 1 2 3
fmt.Println(a[1:2])		// 2
fmt.Println(a[:2])		// 1 2
fmt.Println(a[:3])		// 1 2 3
fmt.Println(a[1:])		// 2 3
fmt.Println(a[0:])		// 1 2 3
fmt.Println(a[:])		// 1 2 3

从数组或切片生成新的切片的特性:

(1)取出的元素数量 = 结束位置 - 开始位置。

(2)取出元素不包含结束位置对应的索引,切片最后一个元素使用slice[len(slice)]获取。

(3)当默认开始位置时,表示从连续区域开头到结束位置。

(4)当默认结束位置时,表示从开始位置到整个连续区域末尾。

(5)两者同时默认时,与切片本身等效。

(6)两者同时为0时,等效于空切片,一般用于切片复位。

(7)根据索引位置取切片slice元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写len(slice)但不会报错。

b. 直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此,切片类型也可以被声明。

语法:var name []Type

  • name表示切片类型的变量名称。
  • Type表示切片类型对应的元素类型。
  • 切片不需要说明长度
var strList []string                                     // 声明字符串切片
var numList []int                                        // 声明整型切片
var numListEmp = []int{}                                 // 声明一个空切片
fmt.Println(strList, numList, numListEmp)                //输出3个切片
fmt.Println(len(strList), len(numList), len(numListEmp)) //输出3个切片大小
// 切片判定空的结果
fmt.Println(strList == nil)		// true
fmt.Println(numList == nil)		// true
fmt.Println(numListEmp == nil)	// false
  • 声明但未使用的切片的默认值是nil,strList和numList也是nil,所以和nil比较的结果是true。
  • numListEmpty已经被分配到了内存,但没有元素,因此和nil比较时是false。

c. make()函数构造切片

如果需要动态地创建一个切片,可以使用make()内建函数。

语法:make( []Type, size, cap )

  • Type是指切片的元素类型。
  • size指的是为这个类型分配多少个元素。
  • cap为预分配的元素数量,该值设定后不影响size,只是能提前分配空间,降低多次分配空间造成性能问题。
// 当使用make()函数时,需要传入一个参数,用于指定切片的长度
a := make([]int, 2)
// 如果只指定长度,那么切片的容量和长度相等。也可以分别指定长度和容量
b := make([]int, 2, 10)
fmt.Println(a, b)           // [0 0] [0 0]
fmt.Println(len(a), len(b)) // 2 2
  • a和b均是预分配两个元素的切片,只是b的内部存储空间已经分配了10个,但实际使用了两个元素。容量不会影响当前的元素个数,因此a和b取len都是2。
  • 分别指定长度和容量时,创建出来的切片的底层数组长度就是创建时指定的容量,但是初始化后并不能访问所有的数组元素。在以上代码中,切片可以访问2个元素,而底层数组拥有10个元素,因此剩余的8个元素可以在后期操作中合并到切片,然后才可以通过切片访问这些元素。
  • 不允许创建容量小于底层数组长度的切片,例如:slice := make([]int,5,3),否则编译会报错。
d. 使用切片字面量创建切片

使用切片字面量创建切片和创建数组类似,只是不需要指定“[]”运算符中的值,初始的长度和容量会根据初始化时提供的元素的个数来确定:

//创建字符串切片
//其长度和容量都是5个元素
slice := []string{ " red ", "blue ", "green ", "yellow ", "pink "}

//创建一个整型切片
//其长度和容量都是3个元素
slice := []int{10,20,30}

当使用切片字面量时,可以设置初始长度和容量,即在初始化时给出所需的长度和容量作为索引(下标)。例如,创建长度和容量都是100个元素的切片。

//使用字符串"1"初始化第100个元素
slice2 := []string{99: "1"}

如果在“[]”运算符中指定了一个值,那么创建的就是数组而不是切片;只有不指定值的时候才会创建切片:

//创建含有3个元素的整型数组
array := [3]int{10,20,30}

//创建长度和容量都是3的整型切片
slice := []int{10,20,30}
e. 创建空(nil)切片

有时程序可能需要声明一个值为空的切片(或nil切片),在声明的时候不做任何初始化,就可以创建一个nil切片:

//创建nil整型切片
var slice []int
  • 一个切片在未初始化之前默认为nil,长度为0。

在Go语言中,nil切片是很常见的创建切片的方法。nil切片多用于标准库和内置函数,在需要描述一个目前暂时不存在的切片时,nil切片十分好用。例如,函数要求返回一个切片但发生异常时,利用初始化,通过声明一个切片可以创建一个nil切片:

//使用make创建空的整型切片
slice := make([]int, 0)

//使用切片字面量创建空的整型切片
slice := [ ]int{}
  • nil切片在底层数组中包含0个元素,也没有分配任何存储空间。
  • nil切片还可以用来表示空集合,例如,数据库查询返回0个查询结果。nil切片和普通切片一样,调用内置函数append()、len()和cap()的效果都是相同的。
2、添加元素:append()

Go语言的内建函数append()可以为切片动态添加元素。每个切片会指向一片内存空间,这片空间能容纳一定数量的元素。当空间不能容纳足够多的元素时,切片就会进行“扩容”。“扩容”操作往往发生在append()函数调用时。

切片在扩容时,容量的扩展规律是按容量的2倍数进行扩充的:

func main() {
	var numbers []int
	for i := 0; i < 10; i++ {
		numbers = append(numbers, i)
		fmt.Printf("len: %d 	cap: %d  pointer:%p\n", len(numbers), cap(numbers), numbers)
	}
}

09.Go切片_第1张图片

append()函数除了可以添加一个元素外,还可以同时添加多个元素:

func main() {
	var student []string
	// 向切片中添加一个元素
	student = append(student, "a")
	fmt.Println(student)

	// 向切片中添加多个元素
	student = append(student, "b", "c", "d")
	fmt.Println(student)

	// 向切片中添加切片
	team := []string{"e", "f", "g"}
	student = append(student, team...)
	fmt.Println(student)
}
  • team后面加上“…”,表示将team整个添加到student的后面。

3、切片的复制

Go语言的内置函数copy()可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片大小不同,那么就会按照较小的数组切片的元素的个数进行复制。

copy()函数的语法:copy( destSlice, srcSlice []Type) int

  • srcSlice为数据来源切片。
  • destSlice为目标切片,目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy()函数的返回值表示实际发生复制的元素个数。
func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := []int{5, 4, 3}

	// 只复制slice1的前3个元素到slice2中
	copy(slice2, slice1)
	fmt.Println(slice2) // [1 2 3]

	// 复制slice2的3个元素到slice1的前3个位置
	copy(slice1, slice2)
	fmt.Println(slice1) // [1 2 3 4 5]
}

切片复制操作后对切片元素的影响:

func main() {
	// 设置元素数量为1000
	const elementCount = 1000
	// 预分配足够多的元素切片
	srcData := make([]int, elementCount)

	// 将切片赋值
	for i := 0; i < elementCount; i++ {
		srcData[i] = i
	}

	// 引用srcData切片
	refData := srcData
	// 预分配足够多的元素切片
	copyData := make([]int, elementCount)
	// 将数据复制到新的切片中
	copy(copyData, srcData)

	// 修改原始数据的第一个袁术:ref改变,copy不变
	srcData[0] = 999
	fmt.Println(refData[0])  // 引用,数据改变
	fmt.Println(copyData[0]) // 赋值,数据不变

	// 赋值原始数据 从4到6(不含6)
	copy(copyData, srcData[4:6]) // copyData 前两个元素为 4 5
	for i := 0; i < 5; i++ {
		fmt.Printf("%d \t", copyData[i])
	}
}
  • 修改原始数据的第一个元素为999。
  • 引用数据的第一个元素将会发生变化。
  • 打印复制数据的首位数据,由于数据是复制的,因此不会发生变化。

4、切片的删除

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置可以分为以下三种情况:

  • 从开头位置删除。
  • 从中间位置删除。
  • 从尾部删除,删除切片元素的速度最快。
a. 从起始位置删除

删除开头的元素可以直接移动数据指针:

a = []int{1, 2, 3}
a = a[1:] //删除开头1个元素
a = a[N:] //删除开头N个元素

删除开头的元素也可以不移动数据指针,但是需要将后面的数据向开头移动,可以使用append()函数原地完成(所谓原地完成,是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化)

a = []int{1, 2, 3}
a = append(a[:0], a[1:]…)  //删除开头1个元素
a = append(a[:0], a[N:]…)  //删除开头N个元素

另外,还可以使用copy()函数来删除开头的元素:

a = []int{1, 2, 3}
a = a[:copy(a, a[1:])]  //删除开头1个元素
a = a[:copy(a, a[N:])]  //删除开头N个元素
b. 从中间位置删除

对于删除中间的元素,需要对剩余的元素进行一次整体的移动,同样可以用append()函数或copy()函数原地完成:

a = []int{1, 2, 3,…}
a = append(a[:i], a[i+1:]…)         //删除中间1个元素
a = append(a[:i], a[i+N:]…)         //删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])]      //删除中间1个元素
a = a[:i+copy(a[i:],a[i+N:])]       //删除中间N个元素
c. 从尾部删除

删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况。

a = []int{1, 2, 3}
a = a[:len(a)-1]  //删除尾部1个元素
a = a[:len(a)-N] //删除尾部N个元素
func main() {
	seq := []string{"a", "b", "c", "d", "e"}
	// 指定删除位置
	index := 2
	// 查看删除位置之前的元素和之后的元素
	fmt.Println(seq[:index], seq[index+1:]) // [a b] [d e]
	
	//将删除点前后的元素连接起来
	seq = append(seq[:index], seq[index+1:]...)
	fmt.Println(seq) //  [a b d e]
}

注意:Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来。

你可能感兴趣的:(Go语言从入门到实践,golang,java,算法)