目录
前言
数组和切片在go语言中的使用十分广泛,其中数组与其他编程语言比较相似,在本文中篇幅可能会较少介绍,而切片作为go语言独有的类型,本文重点讲解对切片的理解与使用
一、数组
1.1 数组的声明和初始化
数组的声明和初始化方式如下:
// 1.数组的声明和初始化
a1 := [2]int{1,2} // 短变量声明,适用于局部变量
//var a1 = [2]int{1,2} // 通过关键字var声明并初始化
a2 := new([2]int) // 注意,new函数返回的是指针
a3 := [...]int{1,2,3} //使用[...]会自动计算数组的长度
a4 := [5]int{1:1,4:4} // 还可以使用key:value的方式对特定位置进行初始化
fmt.Println(a1,*a2,a3,a4) // 输出:[1 2] [1 2] [1 2 3] [0 1 0 0 4]
1.2 数组的类型
go语言中,数组的类型与其他语言不太相似,其类型不仅包括数组内部数据的类型,还包括数组的长度。
// 数组的类型:数组的类型包括其容量和其内部元素的数据类型
fmt.Printf("%T\n",a4) //输出:[5]int,即a4的类型是[5]int,而不是int
此外,数组还是一种值类型 ,即对其进行赋值时,内存会进行一份值拷贝。
// 数组是值类型,赋值的本质是值拷贝
a5 := a4
fmt.Println(a4) //输出:[0 1 0 0 4]
a5[1] = 100
fmt.Println(a5) //输出:[0 100 0 0 4]
fmt.Println(a4) //输出:[0 1 0 0 4]
1.3 数组的遍历
for i:=0;i<len(a1);i++{
fmt.Printf("%v ",a1[i]) // 输出:1 2
}
fmt.Println("")
for _,v := range a2{
fmt.Printf("%v ",v) // 输出:1 2
}
fmt.Println("")
二、切片
2.1切片的声明和初始化
// 切片的初始化方式
s1 := []int{1,2}
s2 := make([]int,0,4) //make(type,len,cap)可用于初始化一个长度为len,容量为cap的切片
a := [2]int{1,2}
s3 := a[:] // [start:end]是一种对数组或切片进行切片的操作,区间是左闭右开的
fmt.Println(s1,s2,s3) //输出:[1 2] [] [1 2]
需要注意的是,当make()函数只传len参数时,默认cap会等于len,我们常用的方法是
s := make(type,0,cap)
2.2 切片的本质与理解
- 切片的本质是对其底层数组的上层封装,我们可以把切片看成是一个框,它框着底层数组,但切片本身是不存放任何数据的,数据存储在其底层数组中,改变切片的值实际上修改的是其底层数组,切片本质如下图所示:
- 切片是引用类型,本质是对其底层数组的引用
// 理解切片的本质:对其底层数组的封装,切片是引用类型 s4 = []int{1,2,3} fmt.Printf("slice s4 len:%v,cap:%v\n",len(s4),cap(s4)) //输出:slice s4 len:3,cap:3 // 本来s4的长度为3,容量为5,但是在赋值变化后,其容量变为3,是由于其底层的数组变了
2.3 切片的长度及容量
- 切片的长度:切片的长度即为切片中所包含的元素的个数
- 切片的容量:切片的容量即切片中第一个元素到其底层数组最后一个元素的距离
- 切片的长度与容量关系如下图所示:
- 代码示例如下:
// 对于切片长度,容量的理解 s4 := make([]int,3,5) // 构造长度为1,容量为5的切片,如果参数只有一个,则len=cap fmt.Printf("slice s4 len:%v,cap:%v\n",len(s4),cap(s4)) //输出:slice s4 len:3,cap:5 s5 := s4[2:] //注意:子切片的索引不能超过其父切片的索引范围,否则会引起报错,比如这里s4[3:]是不对的,因为s4的最大索引为2 fmt.Printf("slice s5 len:%v,cap:%v\n",len(s5),cap(s5)) //输出:slice s5 len:1,cap:3 // 可以看出:切片的长度,就是该切片中元素的个数,切片的容量,是从切片的第一个元素到其底层数组最后一个元素的距离
- 切片的扩容策略:切片是可以进行动态扩容的,其动态扩容遵循一定规则,伪代码如下:
// 扩容标准,假设切片旧容量为old_cap,想要扩容到new_cap,最终的实际容量为cap,则: //1. if new_cap > 2 * old_cap,cap = new_cap //2. else if new_cap < 1024,cap = 2 * old_cap //3. else ,先让cap = old_cap,然后循环叠加:cap += cap/4,直到cap>=new_cap
2.4 两个有关切片的函数append()与copy()
2.4.1 append()函数
- append()函数用于给一个切片末尾追加一个或多个同类型的元素,其定义如下:
func append(slice []Type,elem... Type)[]Type
- append()函数的追加规则:若切片的容量足够追加元素,则在切片末尾进行追加,并返回原来的切片(即底层数组没有改变),如果输入的切片容量不足以追加元素,则会在内存中重新开辟一个足够容量的底层数组,并封装成切片后追加元素并返回(此时返回的切片已经不是原来的切片,其底层数组已经改变)
- append()函数代码示例如下:
// append()函数 s6 := make([]int,1) s7 := []int{1,2,3} fmt.Println(s6,s7) //输出:[0] [1 2 3] fmt.Printf("s6 addr:%p\n",s6 //输出:s6 addr:0xc000016158 s8 := append(s6,s7...) fmt.Println(s8)//输出:[0 1 2 3] fmt.Printf("s8 addr:%p\n",s8) //输出:s8 addr:0xc000020220,与s6已经不同 // 使用append函数追加元素时候,如果切片的底层数组容量不够,则返回的一个新的切片,即其底层数组是在内存中另外开辟的一块容量足够的数组
2.4.2 copy()函数
- 由于切片是引用类型,一旦修改切片的值,其底层数组就会改变,导致所有与该数组相关的切片的值都会改变,牵一发而动全身
- copy()函数会在内存中开辟一块新的数组,并把原切片内容复制给新的切片,其函数定义如下
func copy(dst []Type,src []Type)int
- copy()函数使用示例如下
// copy()函数 // 由于切片是引用类型,一旦修改切片的值,其底层数组就会被改变,导致所有与该数组相关的子切片都会被改变,牵一发而动全身 // 使用copy()函数,可以再内存中复制一块原切片的底层数组,然后返回一个新的切片,这样修改新切片就不会影响原切片及其底层数组 s9 := make([]int,len(s8)) copy(s9,s8) //copy函数不会扩容,dst切片的长度必须大于等于src的长度 fmt.Println(s8,s9) //输出:[0 1 2 3] [0 1 2 3] s9[0] = 100 fmt.Println(s8,s9) //输出:[0 1 2 3] [100 1 2 3]
- 注意:copy函数不会为目的切片自动扩容,它只会根据目的切片的长度,去将原切片中对应长度的内容复制到目的切片。
三、练习
3.1 实现一个在切片任意位置删除任意元素的函数
- 我们首先来看下面这段代码
func delete(s []int,start,end int)([]int,error){
if start < 0 || end > len(s)-1{
return s,errors.New("please input the right index\n")
}
s = append(s[0:start],s[end+1:]...)
return s,nil
}
func main(){
// 下面展示了使用append函数删除切片元素对底层数组的影响
// 可以看到,使用delete()函数删除切片元素后,其底层数组也会被改变
s10 := make([]int,5,10)
for i:=0;i<len(s10);i++{
s10[i] = i
}
fmt.Println(s10) // 输出[0 1 2 3 4]
fmt.Printf("slice s10 len:%v,cap:%v\n",len(s10),cap(s10)) // 输出slice s10 len:5,cap:10
fmt.Printf("s10 addr:%p\n",s10) // 输出:s10 addr:0xc0000180f0
s11,_ :=delete(s10,1,3)
fmt.Println(s11) // 输出[0 4],删除元素成功
fmt.Printf("slice s11 len:%v,cap:%v\n",len(s11),cap(s11)) //输出:slice s11 len:2,cap:10
fmt.Printf("s11 addr:%p\n",s11)// 输出:s11 addr:0xc0000180f0,与s10一样,说明他们的底层数组是一样的
fmt.Println(s10) //输出:[0 4 2 3 4]
}
- 我们通过运行上述代码,发现切片对应位置的元素确实被删除了,但是也引发了一个问题,就是切片所封装的底层数组也被改变,为什么最后输出是[0 4 2 3 4]呢,我们通过下面图示来理解一下上述代码的全过程。
- 首先我们必须知道的一点是,go语言中的函数传参永远都是值拷贝方式传参,也就是说形参是实参的值拷贝,因此,我们可以通过下图解释切片作为参数传入函数时形参与实参的关系
- 随后我们对形参开始使用append()函数进行切片重塑
- 最后,我们把重塑后的切片赋值给形参s,并传出函数。底层数组实际上被改变成了[0 4 2 3 4]
- 那么,我们有没有办法让切片删除指定元素后,不改变原数组的值呢?想要做到这个,就只能另外在数组中开辟一块空间,这样才不会修改到原数组的值。下面给出代码实现
func delete2(s []int,start,end int)([]int,error){ result := make([]int,0,cap(s)) if start < 0 || end > len(s)-1{ return s,errors.New("please input the right index\n") } result = append(result,s[0:start]...) result = append(result,s[end+1:]...) return result,nil }
3.2 实现一个在切片任意位置插入任意元素的函数
- 有了前面删除操作的解释,插入操作的原理也是相似的,这里直接给出代码实现
func insert(s []int,index int,data... int)([]int,error){ result := make([]int,0,cap(s)) if index < 0 || index > len(s) - 1{ return s,errors.New("please input the right index\n") } result = append(result,s[:index]...) result= append(result,data...) s = append(result,s[index:]...) return s,nil }
总结
数组的类型不仅包括其内部的数据类型,还包括其长度,数组是一种值类型,切片实质上是对底层数组的上层封装,其内部不存储数据,数据真正存储在其底层数组,可以将切片理解成一个框,框住底层数组。append()函数在追加元素时,若原切片容量足够,则在其后追加,若容量不够,则在内存中开辟一块新的空间。copy()函数会在内存中开辟一块新的空间,但其不会为目标切片自动扩容,因此使用copy()前必须保证目标切片容量足够。
以上就是本人对go语言中数组和切片的一些浅薄见解,如有错误还望指正。
天道酬勤,让我们一起努力!