跟着老猫来搞GO-容器(1)

前期回顾

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

数组

数组的定义

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

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

var arr1 [5]int //定义一个长度为5的默认类型
arr2:=[3]int{1,2,3} //定义一个数组,并且指定长度为3
arr3:=[...]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

这种方式呢,我们当然是可以获取的。接下来老猫其实还想和大家分享另外一种方式,采用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函数中进行相关调用(为了演示编译错误,老猫这里用图片)

跟着老猫来搞GO-容器(1)_第1张图片

大家根据上面的图可以很清晰的看到调用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 300
1 2
2 3
3 4
4 5

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

0 1
1 2
2 3
3 4
4 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又是从哪里来的呢?

咱们来看一下老猫下面整理的一幅示意图。
跟着老猫来搞GO-容器(1)_第2张图片

  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的底层实现。
跟着老猫来搞GO-容器(1)_第3张图片

其实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)=7
s2=[3 4],len(s2)=2,cap(s2)=5
s3=[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 7

goroutine 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 2

goroutine 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)=3
s2=[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以及字符和字符串的处理。

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

你可能感兴趣的