Golang语言之切片(slice)快速入门篇

                                              作者:尹正杰

版权声明:原创作品,谢绝转载!否则将追究法律责任。

一.切片(slice)概述

1.数组的局限性

数组的三个特点:
	- 1.长度固定;
	- 2.连续内存空间;
	- 3.同一类型集合;

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性,比如数组(array)无法实现扩容和缩容。

2.切片(slice)概述


切片(slice)是Golang中一种特有的数据类型,如上图所示, 切片的本质就是对底层数组的封装,它包含了三个信息:
	- 1.底层数组的指针;
	- 2.切片的长度(len);
	- 3.切片的容量(cap);
	
	
切片是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。支持自动扩容。切片的三个特点:
	- 1.长度可变;
	- 2.连续内存空间;
	- 3.同一类型集合;

切片是数组一个连续片段的引用,所以切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。

这个片段可以是整个数组,或者由起始和终止索引标识符的一些项的子集,终止索引标识的项不包括在切片内。

	
切片和数组的区别:
	- 1.切片长度不固定,可以根据需求自动扩容,数组长度是固定的;
	- 2.数组的长度和容量相等切不可变,而切片的长度和容量并不一定相等;

3.切片的内存分析

package main

import "fmt"

func main() {
	// 定义数组
	var intArray [5]uint8 = [5]uint8{1, 3, 5, 7, 9}

	// 切片构建在数组之上,如果基于数组的索引取切片一定要注意口诀: "前包后不包"。
	var slice []uint8 = intArray[1:4]

	fmt.Printf("intArray数组: %v, 长度: %d, 容量: %d\n", intArray, len(intArray), cap(intArray))

	fmt.Printf("slice切片: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))

	fmt.Printf("intArray[1]数组的内存地址: %p\n", &intArray[1])

	fmt.Printf("slice[0]切片的内存地址: %p\n", &slice[0])
	fmt.Printf("slice[1]切片的内存地址: %p\n", &slice[1])

	// 修改切片的数据
	slice[1] = 88

	// 查看数组和切片的数据是否修改
	fmt.Printf("intArray数组: %v, 长度: %d, 容量: %d\n", intArray, len(intArray), cap(intArray))
	fmt.Printf("slice切片: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))

}

二.切片的三种定义方式

1.切片表达式(基于已经存在的数组来创建切片)

package main

import "fmt"

func main() {

	/*
			切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。

			切片表达式有两种变体:(省略了low则默认为0,省略了high则默认为切片操作数的长度)
		    	(1)一种指定low和high两个索引界限值的简单的形式;
					array[low:high]
		    	(2)另一种是除了low和high索引界限值外还指定容量的完整的形式;
					array[low:high:max]

			完整切片表达式需要满足的条件是0 >= low >= high >= max >= cap(a),其他条件和简单切片表达式相同;

			当设置了max时,则切片的容量设置为"max-low";
	*/

	a := [5]int{1, 2, 3, 4, 5}

	s1 := a[2:] // 等同于 a[2:len(a)]
	s2 := a[:3] // 等同于 a[0:3]
	s3 := a[:]  // 等同于 a[0:len(a)]

	s4 := a[1:3:5]
	s5 := a[1:3:3]

	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
	fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
	fmt.Printf("s4:%v len(s4):%v cap(s4):%v\n", s4, len(s4), cap(s4))
	fmt.Printf("s5:%v len(s5):%v cap(s5):%v\n", s5, len(s5), cap(s5))

}

2.通过make指令创建切片

package main

import "fmt"

func main() {
	/*
		如果需要动态的创建一个切片,我们就需要使用内置的make()函数,格式如下:
			make([]T, size, cap)

		通过make函数常见切片需要传入三个参数:
			T:
				切片的类型
			size:
				切片的长度
			cap:
				切片的容量,容量并不会影响当前元素的个数。 

		make创建切片本质上就是在底层创建了一个数组,该数组对外不可见,所以不可以直接操作这个数组,要通过切片去间接的访问各个元素。
	*/
	slice := make([]int, 4, 20)

	// 为切片赋值
	slice[1] = 100
	slice[3] = 200

	fmt.Printf("切片的长度:%d,容量:%d,数据:%v\n", len(slice), cap(slice), slice)

}

3.声明切片类型

package main

import (
	"fmt"
)

func main() {
	// 声明切片类型,定义一个切片,直接就指定具体数组,使用原理类似make
	var (
		// 声明一个字符串切片
		bigdata = []string{"hadoop", "spark", "flink", "kudu", "hbase", "hive"}

		// 声明一个整型切片并初始化
		scores = []int{99, 88, 77}

		// 声明一个布尔切片并初始化
		svip = []bool{false, true}
	)

	fmt.Printf("bigdata切片的长度:[%d],容量:[%d],数据:%v\n", len(bigdata), cap(bigdata), bigdata)
	fmt.Printf("scores切片的长度:[%d],容量:[%d],数据:%v\n", len(scores), cap(scores), scores)
	fmt.Printf("svip切片的长度:[%d],容量:[%d],数据:%v\n", len(svip), cap(svip), svip)
}

三.切片的遍历

1.基于for循环遍历

package main

import (
	"fmt"
)

func main() {
	s := []byte{'A', 'B', 'C'}

	// 支持基于索引遍历
	for i := 0; i < len(s); i++ {
		fmt.Printf("第[%d]个索引存储的数据是: [%c]\n", i, s[i])
	}
}

2.基于for-range循环遍历

package main

import (
	"fmt"
)

func main() {
	s := []byte{'A', 'B', 'C'}

	// 支持基for-range循环
	for index, value := range s {
		fmt.Printf("第[%d]个索引对应的数据为:%c\n", index, value)
	}
}

四.切片的扩容

1.通过append函数扩容切片

package main

import "fmt"

func main() {
	var (
		// 1.定义数组

		intArr [5]int = [5]int{1, 3, 5, 7, 9}
		// 2.定义切片

		s1 []int = intArr[1:4]
	)

	/*
		切片扩容的底层原理:
			- 1.底层追加元素的时候对数组进行扩容,老数组扩容为新数组;
			- 2.创建一个新数组,将老数组中的s1("3","5","7")复制到新数组中,在新数组中追加"66","88";
			- 3.s2底层数组指向的是新数组;
			- 4.往往我们在使用追加的时候起始想要做的效果给s1追加;
			- 5.底层的新数组不能直接维护,需要通过切片简洁维护操作;
	*/
	s2 := append(s1, 66, 88)

	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))

	s3 := []int{22, 33, 44}

	// append也支持将一个切片直接追加到另一个切片中
	s1 = append(s1, s3...)

	fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))

}

2.切片自动扩容

package main

import (
	"fmt"
)

func main() {

	var numSlice []int
	fmt.Printf("未添加任何元素:%p 数据: %v  长度:%d  容量:%d  \n", numSlice, numSlice, len(numSlice), cap(numSlice))

	for i := 0; i < 10; i++ {
		/*
			温馨提示:
				- 1.每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。
				- 2.当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。
				- 3.“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
		*/
		numSlice = append(numSlice, i)

		fmt.Printf("内存地址指针:%p 数据: %v  长度:%d  容量:%d  \n", numSlice, numSlice, len(numSlice), cap(numSlice))
	}
}

3.切片的扩容策略[了解即可]

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }


从上面的代码可以看出以下内容:
	- 1.首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap),否则走else语句继续判断;
	
	- 2.如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),否则走else语句继续判断;
	
	- 3.如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap);
	
	- 5.如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap);



温馨提示:
	切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。 

五.切片使用注意事项

1.切片使用注意事项

- 1.切片定义后不可以直接使用,需要让其引用到一个数组,或者make一个空间供切片来使用;

- 2.切片使用不能越界;

- 3.切片表达式支持简写形式
	案例1: 
		"var s1 = arr[0:end]"简写为:"var s1 = arr[:end]"
		
	案例2:
		"var s2 = arr[start:len(arr)]"简写为"var s2 = arr[start:]"
		
	案例3:
		"var s3 = arr[0:len(arr)]简写为"var s3 = arr[:]"
		
- 4.切片可以继续切片;

- 5.切片可以动态增长,通过"append"函数操作即可;

- 6.切片也支持使用内置函数copy进行拷贝;

- 7.切片是引用类型,不支持直接比较,切片唯一合法的比较操作是和nil比较

1.切片的赋值拷贝

package main

import (
	"fmt"
)

func main() {
	s1 := make([]int, 3)

	// 切片是引用类型,将s1直接复制给s2,此时s1和s2底层共用同一个数组
	s2 := s1

	fmt.Printf("修改前: s1 --->[%v]\n", s1)
	fmt.Printf("修改前: s2 --->[%v]\n", s2)

	// 注意,我修改的是s2,并没有取修改s1哈
	s2[1] = 100

	// 再次查看s1和s2时你会发现二者都发生了变化哟
	fmt.Printf("修改后: s1 --->[%v]\n", s1)
	fmt.Printf("修改后: s2 --->[%v]\n", s2)

}

2.切片的复制

package main

import (
	"fmt"
)

func main() {
	s1 := []int{11, 22, 33, 44, 55}
	s2 := s1

	// 切片是引用类型,所以s1和s2其实都指向了同一块内存地址。修改s2的同时s1的值也会发生变化。
	s2[1] = 100
	fmt.Printf("修改前: s1 ---> 内存地址: %p [%v], \n", s1, s1)
	fmt.Printf("修改前: s2 ---> 内存地址: %p [%v]\n", s2, s2)

	// 注意,使用make函数对切片进行初始化操作,此操作在"go1.19.3"版本中对s1和s2指向的数组值也有影响哟~
	s3 := make([]int, 3)

	// Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:
	// 		copy(destSlice, srcSlice []T)
	//
	// 温馨提示:
	//		srcSlice:
	//			数据来源切片
	// 		destSlice:
	//			目标切片
	copy(s1, s3) // 使用copy()函数将切片s1中的元素复制到切片s3,属于值拷贝。

	// 由于s3属于只是使用了内置函数copy对s1进行了值拷贝,因此修改s3并不会影响到s1和s2哟~
	s3[2] = 200
	fmt.Printf("s1 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s1, s1, len(s1), cap(s1))
	fmt.Printf("s2 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s2, s2, len(s2), cap(s2))
	fmt.Printf("s3 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s3, s3, len(s3), cap(s3))
}

3.切片元素删除

package main

import (
	"fmt"
)

func main() {

	numberList := []int{11, 22, 33, 44, 55}
	fmt.Printf("删除前 ---> numberList: %v\n", numberList)

	// Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
	// 要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)
	numberList = append(numberList[:3], numberList[4:]...) // 删除索引为3的元素

	fmt.Printf("删除后 ---> numberList: %v\n", numberList)
}

4.切片不能直接比较

package main

import (
	"fmt"
)

func main() {
	var a = []bool{false, true}
	var b = []bool{false, true}

	fmt.Printf("a ---> %v\n", a)
	fmt.Printf("b ---> %v\n", b)

	// 切片是引用类型,不支持直接比较,切片唯一合法的比较操作是和nil比较。
	// fmt.Println(a == b) //  报错: invalid operation: a == b (slice can only be compared to nil)

}

5.判断切片是否为空

package main

import (
	"fmt"
)

func main() {
	var s1 []int
	s2 := []int{}
	s3 := make([]int, 0)
	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
	fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))

	// 温馨提示:
	// 		(1)一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量一定都是0;
	// 		(2)我们不能说一个长度和容量都是0的切片一定是nil;
	fmt.Printf("s1是否为空: %t\n", s1 == nil)
	fmt.Printf("s2是否为空: %t\n", s2 == nil)
	fmt.Printf("s3是否为空: %t\n", s3 == nil)

	// 综上所述,要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。
	fmt.Println(len(s1) == 0)
	fmt.Println(len(s2) == 0)
	fmt.Println(len(s3) == 0)

}

六.练习题

1.观看代码手写运行结果

package main

import (
	"fmt"
)

func main() {
	var a = make([]string, 5, 10)
	fmt.Printf("数据: %v 长度: %d 容量: %d\n", a, len(a), cap(a))

	for i := 0; i < 10; i++ {
		a = append(a, fmt.Sprintf("%v", i))
	}

	fmt.Printf("数据: %v 长度: %d 容量: %d\n", a, len(a), cap(a))
}

2.使用sort包对数组进行排序

package main

import (
	"fmt"
	"sort"
)

func main() {

	var a = [...]int{3, 7, 8, 9, 1}
	fmt.Printf("排序前: %v\n", a)

	sort.Ints(a[:]) // 对切片进行排序,该切片底层对应的就是上面的可变数组哟~
	fmt.Printf("排序后: %v\n", a)
}

posted @ 2024-07-25 23:44  尹正杰  阅读(477)  评论(0编辑  收藏  举报