Golang的高级数据类型-切片(slice)实战篇
作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。
切片(slice)是Go中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合,切片是围绕动态数组的概念构建的,可以按需自动增长。
一.切片的基本使用
1>.切片的定义和初始化赋值
package main import ( "fmt" ) func main() { /* 什么是切片(slice): 切片是Go中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合。 切片是围绕动态数组的概念构建的,可以按需自动增长。 切片的定义格式: 语法一(定义空(nil)切片): var 切片名称 []数组类型 语法二(通过make创建切片): var 切片名称 []数组类型 = make([]数据类型,长度,容量) 语法三(自动推导类型初始化): 切片名称 := []数组类型{元素1,元素2,...,元素N} 切片名称 := make([]数据类型,长度,容量) 切片的长度(len)与容量(cap): len(切片名称): 计算切片有效数据个数,长度是已经初始化的空间,切片初始空间若为被赋值则使用对应数据类型的默认值。 cap(切片名称): 计算切片容量,容量是一家开辟的空间,包括一家初始化的空间和空闲的空间。 常见数组类型的默认值: 整型默认初始化值为0; 浮点型默认初始化值为0; 字符串类型默认初始化值为空串(""); 布尔类型默认初始化值为false; */ //定义一个空(nil)切片,空切片不能添加数据 var slice1 []int fmt.Printf("slice1的数据为:%d,长度为:%d,容量为:%d ", slice1, len(slice1), cap(slice1)) //通过make创建切片 var slice2 []int = make([]int, 3, 5) fmt.Printf("slice2的数据为:%d,长度为:%d,容量为:%d ", slice2, len(slice2), cap(slice2)) //基于切片索引进行赋值时,其索引大小不能等于或超过切片的长度,否则就会报错"index out of range" slice2[1] = 666 fmt.Printf("slice2的数据为:%d,长度为:%d,容量为:%d ", slice2, len(slice2), cap(slice2)) //自动推导类型创建切片 slice3 := []int{1, 2, 3, 4, 5} slice4 := make([]int, 2, 5) //make函数可以理解为给slice4这个切片申请空间 fmt.Printf("slice3的数据为:%d,长度为:%d,容量为:%d ", slice3, len(slice3), cap(slice3)) fmt.Printf("slice4的数据为:%d,长度为:%d,容量为:%d ", slice4, len(slice4), cap(slice4)) }
2>.切片作为函数参数(本质上是引用地址传递)
package main import ( "fmt" ) func Demo(s1 []int32) { //修改切片元素的值 s1[0] = 'Y' fmt.Printf("s1的数据为:[%s],内存地址为:[%p] ", string(s1), s1) } func main() { char := []rune{'尹', '正', '杰', '到', '此', '一', '游'} fmt.Printf("char的数据为:[%s],内存地址为:[%p] ", string(char), char) /* 在Go语言中,数组作为参数进行传递时值传递,而切片作为参数进行传递时引用传递。 值传递: 方法调用时,实参数把他的值传递给对应的形式参数,方法执行中形式参数值的改变不会影响实际参数的值。 引用传递(也称为传地址): 函数调用时,实际参数的引用(地址,而不是参数的值)被传递给函数中相对应的形式参数(实参与形参指向了同一块存储区域); 在函数执行时,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变会影响实际参数的值。 温馨提示: 建议在开发中使用切片代替数组。 */ Demo(char) fmt.Printf("char的数据为:[%s],内存地址为:[%p] ", string(char), char) //主线程调用函数切片元素的值被修改 }
3>.切片中的指针问题刨析
package main import ( "fmt" ) func main() { //定义一个切片 s1 := []rune{'尹', '正', '杰'} /* C++转过来学Go还容易理解指针,但是从Python和Java转过来学Go时接触Go语言的小伙伴是不是很抵触指针呢? 我很好奇格式化输出有一个"%p",但是我传递切片"s1"和"&s1"是不一样的,这是为什么呢? */ fmt.Printf("char的数据为:[%s],内存地址为:[%p] ", string(s1), s1) fmt.Printf("char的数据为:[%s],内存地址为:[%p] ", string(s1), &s1) /* 指针问题刨析: 切片名s1是指向内存空间,所以s1其实存的是切片在内存中的存储地址。 而"&s1"其实打印的是存储地址空间的地址(即s1变量本身的地址),换句话说,s1本身也是一个变量,它也有自己的内存地址。 */ }
问题:
我们在使用"fmt"包中"Printf"方法中的"%p"进行格式化时,为什么数组都需要加取值符("&"),而切片却不用呢?
问题刨析:
首先,不是"%p"就一定能够打印地址,变量本身存储的是地址,才能打印地址。接下来我们分析一下数组和切片的关系: 数组是可以直接存储数据的,如果想要打印数组的地址,首先得使用取值符("&")来取地址。 切片并不是直接存储数据的,切片需要先使用make申请空间,然后得到一个空间的引用地址才能基于该引用地址存储数据。也就是说切片本身就是一个引用地址。
二.切片扩容
package main import ( "fmt" ) func main() { /* 切片的扩容: 切片的长度时不固定的,可以向已经定义的切片中追加数据。 通过append函数可以在原切片的尾部添加元素 切片的结构如下所示,其中unsafe.Pointer对应的类型为: type slice struct { array unsafe.Pointer len int cap int } 接下来我们来刨析一下slice结构体: array: 存储的是数组指针,它指向了数组的内存地址。 其类型"unsafe.Pointer"对应“type Pointer *ArbitraryType”,而"ArbitraryType"对应的是"int",而int在64为操作系统上来讲就是int64,对应8字节。 len: int类型对应的是8字节 cap: int类型对应的是8字节 */ s1 := []int{100, 200, 300} //使用自动推导类型定义切片 fmt.Printf("s1的长度为:%d,容量为:%d,内存地址为:%p ", len(s1), cap(s1), s1) /* 向slice1切片中添加(扩容)了5个元素,尽管slice1之前的容量是3,但依旧是可以往里面添加数据的哟~而且并不会改变slice1的内存地址。 */ s1 = append(s1, 100) fmt.Printf("s1的长度为:%d,容量为:%d,内存地址为:%p ", len(s1), cap(s1), s1) s1 = append(s1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) fmt.Printf("s1的长度为:%d,容量为:%d,内存地址为:%p ", len(s1), cap(s1), s1) /* 如果切片扩容超过切片的容量,那么Go语言会该切片重新申请一个内存空间,把原来切片中的数据拷贝到新的地址空间,因此在生产环境中建议大家不要将切片的容量设置过小。 切片的扩容规则(参考源码文件"runtime/slice.go"的"func growslice(et *_type, old slice, cap int) slice"函数): 如果长度和容量相等分为下面三种情况: 1>.长度和容量小于等于896时: 再次为切片添加数据时,切片会自动扩容,扩容大小为上一次的2倍。 2>.长度和容量大于896小于1024时: 再次为切片添加数据时,切片会自动扩容,扩容大小固定为2048。 3>.长度和容量大于等于1024时: 再次为切片添加数据时,切片会自动扩容扩容为上一次的四分之一(1/4)。 */ s2 := make([]int, 896, 896) fmt.Printf("s2的长度为:%d,容量为:%d,内存地址为:%p ", len(s2), cap(s2), s2) s2 = append(s2, 1) // fmt.Printf("s2的长度为:%d,容量为:%d,内存地址为:%p ", len(s2), cap(s2), s2) s3 := make([]int, 897, 897) fmt.Printf("s3的长度为:%d,容量为:%d,内存地址为:%p ", len(s3), cap(s3), s3) s3 = append(s3, 2) // fmt.Printf("s3的长度为:%d,容量为:%d,内存地址为:%p ", len(s3), cap(s3), s3) s4 := make([]int, 1023, 1023) fmt.Printf("s4的长度为:%d,容量为:%d,内存地址为:%p ", len(s4), cap(s4), s4) s4 = append(s4, 2) // fmt.Printf("s4的长度为:%d,容量为:%d,内存地址为:%p ", len(s4), cap(s4), s4) s5 := make([]int, 1024, 1024) fmt.Printf("s5的长度为:%d,容量为:%d,内存地址为:%p ", len(s5), cap(s5), s5) s5 = append(s5, 2) // fmt.Printf("s5的长度为:%d,容量为:%d,内存地址为:%p ", len(s5), cap(s5), s5) s6 := make([]int, 1025, 1025) fmt.Printf("s6的长度为:%d,容量为:%d,内存地址为:%p ", len(s6), cap(s6), s6) s6 = append(s6, 2) fmt.Printf("s6的长度为:%d,容量为:%d,内存地址为:%p ", len(s6), cap(s6), s6) }
三.切片的截取
package main import "fmt" func main() { /* 切片的截取: 所谓截取就是从切片中获取指定的数据。 切片常见的操作如下所示: "slice[n]": 切片slice中索引位置的为n的选项。 "slice[:]": 从切片slice的索引位置0到len(slice)-1处获得的切片 "slice[low:]": 从切片slice的索引位置low到len(slice)-1处获得的切片 "slice[:high]": 从切片selice的索引位置0到high处所获得的切片,len=high-low "slice[low:high:max]": 从切片slice的索引位置low到high处所获得的切片,len=high-low,cap=max-low "len(s)": 表示切片s的长度,切片的长度总是小于等于(>=)切片容量的(cap(s)) "cap(s)": 表示切片s的容量,切片的容量总是大于等于(<=)切片的长度(len(s)) */ slice1 := []rune{'尹', '正', '杰', '到', '此', '一', '游'} fmt.Printf("slice1的数据为:[%s],长度为:[%d],容量为:[%d] ", string(slice1), len(slice1), cap(slice1)) s1 := slice1[2] //获取切片索引为2的元素 fmt.Printf("s1的数据为:[%s] ", string(s1)) s2 := slice1[:] //默认截取的长度和容量相等 fmt.Printf("s2的数据为:[%s],长度为:[%d],容量为:[%d] ", string(s2), len(s2), cap(s2)) s3 := slice1[3:] //设置截取的起始索引,左闭右开 fmt.Printf("s3的数据为:[%s],长度为:[%d],容量为:[%d] ", string(s3), len(s3), cap(s3)) s4 := slice1[:4] //设置截取的结束索引,左闭右开 fmt.Printf("s4的数据为:[%s],长度为:[%d],容量为:[%d] ", string(s4), len(s4), cap(s4)) s5 := slice1[1:3] //设置截取的起始索引和结束索引,左闭右开 fmt.Printf("s5的数据为:[%s],长度为:[%d],容量为:[%d] ", string(s5), len(s5), cap(s5)) s6 := slice1[1:3:6] //设置截取的起始索引和结束索引并指定容量 fmt.Printf("s6的数据为:[%s],长度为:[%d],容量为:[%d] ", string(s6), len(s6), cap(s6)) }
四.切片的浅拷贝(虽然Go语言支持二维数组,但不建议大家使用,推荐使用字典)
package main import ( "fmt" ) func main() { /* Go语言的内置函数copy()可以将一个切片复制到另一个切片。 如果加入的两个数组切片不一样,就会按其中较小的那个数组切片的元素个数进行复制。 */ src := []int32{'尹', '正', '杰', '到', '此', '一', '游'} fmt.Printf("src的数据为:[%s],长度为:[%d],容量为:[%d],内存地址为:[%p] ", string(src), len(src), cap(src), src) //创建一个长度为5的元素大小的切片 dest := make([]rune, 5) //切片拷贝,会根据较小的切片进行拷贝,由于dest的容量为5,而src的容量为7,将src的元素拷贝到dest时会按照容量较小的(即dest的容量)来进行拷贝 copy(dest, src) fmt.Printf("dest的数据为:[%s],长度为:[%d],容量为:[%d],内存地址为:[%p] ", string(dest), len(dest), cap(dest), dest) }
五.删除切片(生产环境中不推荐大家删除切片的元素,如果非要删除建立考虑从尾部删除,具体原因看下面案例你就知道啦~)
package main import ( "fmt" ) func main() { /* Go语言切片删除: Go语言并没有对删除切片提供相对应的函数,需要使用切片本身的特性来删除元素。 切片删除的本质: 以被删除的元素为起点,到删除的元素为终点,将前后两部分数据在内存重新连接起来。 当删除一个切片的第一个元素时,那么第一个元素的存储空间会被释放,但此时后面的所有元素都得集体往前移动一个元素哟,这是很消耗性能的,尤其是在数据量大的情况下; 当删除一个切片的最后一个元素时,那么最后一个元素的存储空间也会被释放,由于最后一个元素后面没有元素啦,因此不会产生数据的移动。 温馨提示: 如果切片元素过多,整个删除过程非常消耗性能,因为删除切片前面的元素,那么该切片前面的元素空出来后,后面的元素依次往前面移动,如果数据量很大的情况下效率极低。 生产环境中,不建议大家对数据量较大的切片进行元素删除操作,如果数据有频繁的删除操作哦,建议换其它数据存储容器。 删除切片的数据并不会修改切片的容量大小。 */ s1 := []rune{'2', '0', '2', '0', '尹', '正', '杰', '来', '也'} fmt.Printf("删除前s1的数据为:[%s],长度为:[%d],容量为:[%d],内存地址为:[%p] ", string(s1), len(s1), cap(s1), s1) fmt.Printf("删除前s1各元素地址:[%p] [%p] [%p] [%p] [%p] [%p] [%p] [%p] [%p] ", &s1[0], &s1[1], &s1[2], &s1[3], &s1[4], &s1[5], &s1[6], &s1[7], &s1[8]) s1 = append(s1[:0], s1[4:]...) //删除下标前面四个的元素,左闭右开(即顾左不顾右),需要使用不定参格式 fmt.Printf("删除后s1的数据为:[%s],长度为:[%d],容量为:[%d],内存地址为:[%p] ", string(s1), len(s1), cap(s1), s1) fmt.Printf("删除后s1各元素地址:[%p] [%p] [%p] [%p] [%p] ", &s1[0], &s1[1], &s1[2], &s1[3], &s1[4]) }