正如我们在 数组 了解到的,数组在诸多方面并不是很灵活,应用的非常之少。那么今天要学习的切片,相对的则是随处可见的,Go语言中,切片是一种建立在数组类型之上的抽象,它构建在数组之上并且提供更强大的能力和便捷。
一、基本概念
切片(Slice)代表变长的序列,序列中每个元素都有相同的类型。slice类型一般写作[]T,其中T代表slice中元素的类型(和数组的区别就是[]没有了数值,即容量)。举例如下:
var sli []int
var months []string
我们可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组(匿名的数组变量)。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。
一个slice由三个部分构成:指针、长度和容量:
- 指针:指向第一个slice元素对应的底层数组元素的地址
- 长度:对应slice中元素的数目,长度不能超过容量,可以用内置函数len得到
- 容量:一般是从slice的开始位置到底层数据的结尾位置,可以使用内置函数cap得到
特别说明:如果 slice == nil,那么 len、cap 结果都等于 0。
还有一点,若果是空切片,它的len和cap也是0,但是和nil切片指向底层数组的指针不同:nil切片的指针是nil,而空切片对应的指针是个地址。
多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。
二、切片的创建和使用
//通过make创建
//创建一个长度为5,容量为10的切片
//如果不指定容量,那么容量和长度相等
slice := make([]int, 5, 10)
//通过字面量创建
myStr := []string{"Jack", "Mark", "Nick"}
myNum := []int{10, 20, 30, 40}
//只初始化某个索引的值,其他索引默认零值
myNums := []int{4:1}
//获取值
fmt.Println(myNum[2])
//修改值
myNum[2] = 5
//越界访问会导致运行时异常 panic: out of range
fmt.Println(myNum[10])
可以注意到,通过字面量声明切片时跟声明数组非常相似,唯一的区别就是声明切片的时候[]运算符里没有指定明确的数字(容量)。
slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。除了[]byte类型可以使用bytes.Equal函数来判等之外,其他的任何切片都需要手动展开每一项进行比较。
slice唯一合法的比较操作是和nil比较。
func equal(x, y []string) bool {
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}
之所以不支持切片比较,主要基于以下两个原因:
- slice的元素是间接引用的,一个slice甚至可以包含自身,比较起来非常复杂
- 一个固定的slice值(slice本身)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。
三、切片的内存结构
我们用下面这个例子来说明下切片在内存中的结构是怎样的。
months := [...]string{1: "January", /* ... */, 12: "December"}
Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2) // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]
slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。所以Q2和summer有一个月的重叠部分,结构见下图:
有一个有意思的写法,你可能没有见到过,其实切片的这个操作还可以有“第三个参数”:
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5] // [2 3 4]
s2 := s1[2:6:7] // [4 5 6 7]
fmt.Println(s1, s2, len(s2), cap(s2))
//[2 3 4] [4 5 6 7] 4 5
在这个写法中,“第三个参数”标识的是容量,即容量结束的位置。比如上面示例中,这个7,代表容量从2 -> 7,所以,这时候就是容量从2~7是它的开始和结束位置。另外需要注意,SLICEX[A:B:C] 这种写法时,B和C不要超过SLICEX的容量即可,与length无关。
四、append()函数
内置的append函数用于向slice追加元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时。
slice := []int{1, 2, 3, 4, 5}
newSlice := slice[1:3]
newSlice=append(newSlice,10)
//同时追加多个
newSlice=append(newSlice,20,30,40)
//追加一个切片到另一个切片中
newSlice2 := slice[2:4]
newSlice=append(newSlice,newSlice2…)
目前的扩容策略是:容量小于1024个时,总是成倍的增长,一旦容量超过1024个,增长因子设为1.25,也就是说每次会增加25%的容量。如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准。
五、copy()函数
由于切片是引用类型,所以通过普通的赋值操作,会指向同一个内存地址,对“新变量”的修改,也会影响到“老变量”的值,那么怎么办呢?这时就可以使用内置的copy()函数了。
// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
六、删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素:要从切片a中删除索引为index的元素,操作方法是 a = append(a[:index], a[index+1:]...)
//从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
七、切片排序
如果我们相对一个切片,按照其中某个字段,进行排序,要怎么实现呢?可以参考下面的示例完成排序:
package main
import (
"fmt"
"sort")
type Person struct {
Age int
Name string
}
func main() {
var personArr []*Person
p1 := &Person{Age: 1, Name: "小明"}
p2 := &Person{Age: 5, Name: "小红"}
p3 := &Person{Age: 4, Name: "小李"}
p4 := &Person{Age: 2, Name: "小杰"}
personArr = append(personArr, p1)
personArr = append(personArr, p2)
personArr = append(personArr, p3)
personArr = append(personArr, p4)
//按照最大值排序
sort.Slice(personArr,func (i int,j int) bool {
return personArr[i].Age < personArr[j].Age
})
for _,v := range personArr {
fmt.Println(v)
}
}
如果是一个简单的整形切片,可以如下面的例子完成排序:
package main
import "fmt"
/*对切片排序*/
func SortSlice(slice []int) {
for i := 0; i < len(slice)-1; i++ {
//遍历i位以后的所有元素,如果比i位元素小,就和i位元素互换位置
for j:=i+1;j < len(slice) ;j++ {
if slice[j] < slice[i]{
slice[i],slice[j] = slice[j],slice[i]
}
}
}
}
func main() {
slice := []int{6, 8, 3, 5, 9, 7, 1, 4, 2, 0}
SortSlice(slice)
fmt.Println(slice)
}
好,切片相关的知识点就总结到这里,接下来进入 字典 的学习吧!