后期回顾

后面的一章次要和大家分享了GO语言的函数的定义,以及GO语言中的指针的简略用法,那么本章,老猫就和大家一起来学习一下GO语言中的容器。

数组

数组的定义

说到容器,大家有编程教训的必定第一个想到的就是数组了,当然也有编程教训的小伙伴会感觉数组并不是容器。然而无论如何,说到数组其实它就是存储和组织数据的一种形式而已,大家就不要太过纠结叫法了。

咱们间接上数组定义的例子,具体如下:

var arr1 [5]int //定义一个长度为5的默认类型arr2:=[3]int{1,2,3} //定义一个数组,并且指定长度为3arr3:=[...]int{1,2,3,4,5,6} //定义一个数组,具体的长度交给编译器来计算var grid [4][5] bool //定义一个四行五列的二维数组fmt.Println(arr1,arr2,arr3,grid)

下面的例子输入的后果如下

[0 0 0 0 0] [1 2 3] [1 2 3 4 5 6] [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

大家能够总结一下,其实数组有这么几个特点

  • 在写法上,其实也是和其余编程语言是相同的,其定义的数组的长度写在变量类型的后面
  • 数组中所存储的内容必然是同一类型的

数组的遍历

那么咱们如何遍历获取数组中的数据呢?其实看过老猫之前文章的小伙伴应该知道能够用for循环来遍历获取,其中一种大家比拟容易想到的形式如下(咱们以遍历下面的arr3为例)

for i:=0;i<len(arr3);i++ {    fmt.Println(arr3[i])}

这种形式呢,咱们当然是能够获取的。接下来老猫其实还想和大家分享另外一种形式,采纳range关键字的形式

//i示意的是数据在数组中的地位下标,v示意理论的值for i,v :=range arr3 {    fmt.Println(i,v)}//那么如果咱们只想要value值呢,回顾一下老猫之前所说的就能够知道,咱们能够用_的形式进行对i省略for _,v :=range arr3 {    fmt.Println(v)}//如果咱们只有地位下标,那么咱们如上来写即可for i:=range arr3 {    fmt.Println(i)}

大家感觉上述两种形式哪种形式会比拟优雅?不言而喻是后者了,意义明确而且好看。

go语言中数组是值传递的

另外和大家同步一点是数组作为参数也是值传递。还是沿用之前的咱们从新定义一个新的函数如下:

func printArray(arr [5]int){    for i,v:=range arr {        println(i,v)    }}

那么咱们在main函数中进行相干调用(为了演示编译谬误,老猫这里用图片)

大家依据下面的图能够很清晰的看到调用printArray(arr2)的时候报了编译谬误,其实这就是说明,在go语言中,即便同一个类型的数组,如果不同长度,那么编译器还是认为他们是不同类型的。

那么咱们这个时候再对传入的数组进行值的变更呢,具体如下代码

func main() {    arr3:=[...]int{1,2,3,4,5} //定义一个数组,并且长度可变    printArray(arr3)    for i,v:=range arr3 {        println(i,v)    }}func printArray(arr [5]int){    arr[0] = 300    for i,v:=range arr {        println(i,v)    }}

大家能够看到,老猫在这里操作了两次打印,第一次打印是间接在函数中打印,此时曾经更改了第一个值,其函数外部打印的后果为

0 3001 22 33 44 5

显然外部的值是变更了,然而咱们再看一下里面的函数的打印的值,如下

0 11 22 33 44 5

其实并没有产生变更,这其实阐明了什么呢,这其实阐明了在调用printArray的时候其实是间接将数组拷贝一份传入函数的,里面的数组并未被更新,这也间接阐明了GO语言是值传递的参数传递形式。

大家在应用这个数组的时候肯定要留神好了,说不准就被坑了。大家可能会感觉这个数组真难用,其实能够通知大家一个好消息,在GO语言中,个别其实不会间接去应用数组的,咱们用的比拟多的还是“切片”

切片

说到切片的话,咱们其实最好是基于下面数组的根底下来了解切片。咱们先来看一个例子

func main() {    arr := [...]int{1,2,3,4,5,6,7}    fmt.Println("arr[2:6]",arr[2:6])    fmt.Println("arr[:6]",arr[:6])    fmt.Println("arr[2:]",arr[2:])    fmt.Println("arr[:]",arr[:])}

其实像相似于'[]'这种定义咱们就称说其为切片,英文成为slice,它示意领有雷同类型元素的可变长度的序列。咱们来看一下后果:

arr[2:6] [3 4 5 6]arr[:6] [1 2 3 4 5 6]arr[2:] [3 4 5 6 7]arr[:] [1 2 3 4 5 6 7]

其实这么说会比拟好了解,slice咱们能够将其看作为视图,就拿arr[2:6]来说,咱们其实在原来数组的根底上抽取了从第二个地位到第六个地位的元素作为值从新展示进去,当然咱们的取值为左闭右开区间的。

slice其实是视图概念

下面咱们说了slice相当于是数组的视图,那么接下来的例子,咱们来证实上述的说法,具体看上面的例子

func main() {    arr := [...]int{1,2,3,4,5,6,7}    fmt.Println("arr[2:6]",arr[2:6])    updateSlice(arr[2:6])    fmt.Println("arr[2:6]",arr[2:6])    fmt.Println(arr)}func updateSlice(arr []int){    arr[0] = 100}

老猫写了个函数,次要是更新slice第一个地位的值,大家能够先思考一下执行前后所失去的后果是什么,而后再看上面的答案。

其实最终执行的后果为:

arr[2:6] [3 4 5 6]arr[2:6] [100 4 5 6][1 2 100 4 5 6 7]

那么为什么是这样的?其实arr[2:6]很容易了解是下面的3456,第二个也比拟容易了解,当咱们slice的第一个值被更新成了100,所以编程了第二种,那么原始的数据为什么也会变成100呢?这外面其实是须要好好品一下,因为咱们之前说slice是对原数组的视图,当咱们第二种看到slice其实曾经产生了更新变成了100,那么底层的数据必定也产生了变更,变成了100了。(这里要留神的是,并没有谁说视图的操作不会副作用于原数组)。这里还是比拟重要的,心愿大家细品一下。

reslice以及扩大

说到reslice,说白了就是对原先的slice再做一次slice取值,那么咱们看上面的例子。

func main() {    arr := [...]int{1,2,3,4,5,6,7}    s1 := arr[:]    s2 := s1[2:4]    fmt.Println(s2)}

以上例子可见s1是对数组的全量切片,而后咱们对s1又进行了一次切片解决,很容易地能够推算出来咱们第二次所失去的后果为[3,4],像这种行为咱们就称为reslice,这个还是比拟好了解的。

接下来咱们在这个根底上加深一下难度,咱们在S2的根底上再次进行resilce,具体如下:

func main() {    arr := [...]int{1,2,3,4,5,6,7}    s1 := arr[:]    s2 := s1[2:4]    s3 := s2[1:3]    fmt.Println(s3)}

咱们都晓得s2所失去的值为[3,4],当咱们在次对其进行reslice的时候,因为取的是[1:3],那么此时咱们发现是从第一个地位到第三个地位,第一个地位还是比拟好推算出来的,基于[3,4]的话,那么其第一个地位应该是4,那么前面呢?后果又是什么呢?这里将后果间接通知大家吧,其实老猫运行之后所失去的后果是

[4 5]

那么为什么会有这样的一个后果?5又是从哪里来的呢?

咱们来看一下老猫上面整顿的一幅示意图。

  1. arr的一个数组,并且其长度为7,并且外面存储了七个数。
  2. 接下来s1对其去齐全切片,所以咱们失去的也是一个残缺的7个数。
  3. 须要留神的是,这时候咱们用的是下标示意,当s2对s1在此切片的时候,咱们发现其本质是对数组的第二个元素开始进行取值,因为是视图的概念,其实s2还会视图arr空幻出另外两个地位,也就是咱们示意的灰色的3以及4下标。
  4. 同样的咱们将s3示意进去,由此咱们s3是在s2的根底上再次切片,实践上有三个下标值,别离是0、1、2下标取值,然而咱们发现s2的3号地位批示空幻进去的地位,并未真正存在值与之对应,因而,咱们取交加之后与数组arr对应只能取出两个,也就是最终的[4,5]。

此处还是比拟难了解,心愿大家好好了解一下,而后写代码本人推演一下,其实这个知识点就是slice的扩大,咱们再来看一下上面的slice的底层实现。

其实slice个别蕴含三个概念,slice的底层其实是空数组构造,ptr为指向数组第一个地位的指针,Len示意具体的slice的可用长度,而cap示意有能力扩大的长度。

其实对于len以及cap咱们都有函数间接能够调用获取,咱们看一下下面的例子,而后打印一下其长度以及扩大cap大家就分明了。具体打印的代码如下。

func main() {    arr := [...]int{1,2,3,4,5,6,7}    s1 := arr[:]    s2 := s1[2:4]    s3 := s2[1:3]    fmt.Printf("arr=%v\n",arr)    fmt.Printf("s1=%v,len(s1)=%d,cap(s1)=%d\n",s1,len(s1),cap(s1))    fmt.Printf("s2=%v,len(s2)=%d,cap(s2)=%d\n",s2,len(s2),cap(s2))    fmt.Printf("s3=%v,len(s3)=%d,cap(s3)=%d\n",s3,len(s3),cap(s3))}

上述代码输入的后果为

arr=[1 2 3 4 5 6 7]s1=[1 2 3 4 5 6 7],len(s1)=7,cap(s1)=7s2=[3 4],len(s2)=2,cap(s2)=5s3=[4 5],len(s3)=2,cap(s3)=4

当咱们的取值超过cap的时候就会报错,例如当初s2为s2:=[2:4],当初咱们发现其cap为5,如果咱们超过5,那么此时s2能够写成s2:=[2:8],那么此时就会报以下异样

panic: runtime error: slice bounds out of range [:8] with capacity 7goroutine 1 [running]:main.main()    E:/project/godemo/part6-slice.go:8 +0x7f

再者如果咱们这么取值

fmt.Printf("s3=%v",s3[4])

此时s3曾经超过了len长度,那么也会报错,报错如下

panic: runtime error: index out of range [4] with length 2goroutine 1 [running]:main.main()    E:/project/godemo/part6-slice.go:14 +0x49f

综上例子,咱们其实能够失去这么几个论断。

  1. slice能够向后扩大,不能够向前扩大。
  2. s[i]不能够超过len(s),向后扩大不能够超过底层数组cap(s)

以上对slice的扩大其实还是比拟让人头疼的,比拟难了解,不过真正弄清外面的算法倒是也还好,心愿大家也能了解上述的阐释,老猫曾经尽最大致力了,如果还有不太分明的,也欢送大家私聊老猫。

切片的操作

向slice增加元素,如何增加呢?看一下老猫的代码,如下:

func main() {    arr :=[...]int{0,1,2,3,4,5,6,7}    s1 :=arr[2:6]    s2 :=s1[3:5]    s3 := append(s2,10) //[5,6,10]    s4 := append(s3,11) //[5,6,10,11]    s5 := append(s4,12)    fmt.Printf("arr=%v\n",arr)    fmt.Printf("s2=%v,len(s2)=%d,cap(s2)=%d\n",s2,len(s2),cap(s2))    fmt.Printf("s2=%v\n",s2)    fmt.Printf("s3=%v\n",s3)    fmt.Printf("s4=%v\n",s4)    fmt.Printf("s5=%v\n",s5)}

如上述所示,咱们往切片中增加操作的时候采纳的是append函数,大家能够先不看老猫上面的理论后果本人推算一下最终的输入后果是什么。联合之前老猫所述的切片操作。后果如下:

arr=[0 1 2 3 4 5 6 10]s2=[5 6],len(s2)=2,cap(s2)=3s2=[5 6]s3=[5 6 10]s4=[5 6 10 11]s5=[5 6 10 11 12]

上述咱们会发现append操作的话会有这样的一个论断

  • 增加元素的时候如果超过cap,零碎会重新分配更大的底层数组
  • 因为值传递的关系,必须接管append的返回值

slice的创立、拷贝

之前老猫和大家分享的slice看起来都是基于arr的,其实slice的底层也的确是基于arry的,那么咱们是不是每次在创立slice的时候都须要去新建一个数组呢?其实不是的,咱们slice的创立形式有很多种,咱们来看一下上面的创立形式

func main() {    var s []int //1、空slice的创立形式,其实底层是基于Nil值的数组创立而来    for i := 0;i<100;i++ {        s = append(s,2*i+1)    }    fmt.Println(s)    s1 :=[]int {2,4,5,6} //2、创立一个带有初始化值得slice    s2 :=make([]int ,16) //3、采纳make内建函数创立一个长度为16的切片    s3 :=make([]int,10,32) //4、采纳make内建函数创立一个长度为10的切片,然而cap为32    //slice的拷贝也是相当简略的也是间接用内建函数即可,如下    copy(s2,s1) //这里次要示意的是将s1拷贝给s2,这里须要留神的是不要搞反了}

slice元素的删除操作

为什么要把删除操作独自拎进去分享,次要是因为上述这些操作都有比拟便捷的内建函数来应用,然而删除操作就没有了。咱们只能通过切片的个性来求值。如下例子

func main() {    s1 :=[] int{2,3,4,5,6}    s2 :=append(s1[:2],s1[3:]...)    fmt.Println(s2)}

上述有一个2到6的切片,如果咱们要移除其中的4元素,那么咱们就得用这种切片组合的形式去移除外面的元素,置信大家能够看懂,至于“s1[3:]...”这种模式,其实是go语言的一种写法,示意取从3号地位剩下的所有的元素。

最终咱们失去的后果就失去了

[2 3 5 6]

以上就是对slice的所有的常识分享了,花了老猫不少工夫整理出来的,老猫也尽量把本人的一些了解说分明,slice在语言中还是比拟重要的。

写在最初

回顾一下下面的GO语言容器,其实重点和大家分享是slice(切片)的相干定义,操作以及底层的一些原理。弄清楚的话还是比拟容易上手的。当然go语言的容器可不止这些,因为篇幅的限度,老猫就不分享其余的容器了,置信在写下去就没有急躁看了。前面的容器次要会和大家分享map以及字符和字符串的解决。

我是老猫,更多内容,欢送大家搜寻关注老猫的公众号“程序员老猫”。