前期回顾

前面的一章主要和大家分享了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<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 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又是从哪里来的呢?

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

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

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

跟着老猫来搞GO-容器(1)的更多相关文章

  1. 跟着老猫来搞GO-内建容器Map

    前期回顾 在上面的文章中,老猫和大家分享了GO语言中比较重要的两种数据结构,一种是数组,另外一种是基于数组的slice.本篇文章想要继续和大家分享剩下的容器以及字符字符串的处理. MAP map的定义 ...

  2. 跟着老猫来搞GO,集跬步而致千里

    上次博客中,老猫已经和大家同步了如何搭建相关的GO语言的开发环境,相信在车上的小伙伴应该都已经搞定了环境了.那么本篇开始,我们就来熟悉GO语言的基础语法.本篇搞定之后,其实期待大家可以和老猫一样,能够 ...

  3. 跟着老猫来搞GO,"面向对象"

    前言 之前和大家分享了容器以及相关的基础语法,以及函数,相信如果大家有接触过C++或者java的朋友都晓得面向对象,其实在GO语言中也存在面向对象,但是还是比较简单的,下面我们来看一下GO语言的&qu ...

  4. 跟着老猫来搞GO,基础进阶

    回顾一下上一篇博客,主要是和大家分享了GO语言的基础语法,其中包含变量定义,基本类型,条件语句,循环语句.那本篇呢就开始和大家同步一下GO语言基础的进阶. 函数的定义 上次其实在很多的DEMO中已经写 ...

  5. 跟着老猫一起来学GO,环境搭建

    老猫的GO学习系列博客已经正式发车了,相信大家以前学习一门编程语言的时候也有经验,咱们一般都是从环境开始,在此呢,大家也跟着老猫从最开始的搭建环境开始. GO语言的安装 首先呢,我们开始需要下载GO语 ...

  6. 我用Axure制作了一款火影小游戏 | PM老猫

    Axure不仅仅是一个原型工具,除了原型之外还可以用来制作一些静态网页,这点对于不懂代码或前端的同学来说挺实用.之前整理了一版<Axure函数自查表>,因为感觉内容太多又对前端样式及脚本感 ...

  7. 跟着老王学Python

    亲爱的朋友:     欢迎你!很高兴能在这里见到你,你能来到这里说明你真的很喜欢python,很想把python给学好!我觉的你很幸运,开始我学python的时候比较少资料,学起来也比较头疼,现在随着 ...

  8. 【vue】跟着老马学习vue-数据双向绑定

    学习了node.js教程,只能说是有了一定的了解,之前也了解了webpack和es6的核心内容,也看过vue2.0的官网教程,并结合视频看过项目,但是理解和运用仍然存在很多问题,接下来的一段时间,跟着 ...

  9. 一张图搞懂容器所有操作 - 每天5分钟玩转 Docker 容器技术(26)

    前面我们已经讨论了容器的各种操作,对容器的生命周期有了大致的理解,下面这张状态机很好地总结了容器各种状态之间是如何转换的. 如果掌握了前面的知识,要看懂这张图应该不难.不过有两点还是需要补充一下: 可 ...

随机推荐

  1. 什么鬼?你还搞不懂json和字典的区别??

    现在自动化培训烂大街,是个人都能说的上几个框架,面试如果问框架相关问题,求职者只需一瓶 82 年的雪碧,会吹的让你怀疑人生!所以面试官为了更清楚的知道你是停留在表面上的花拳绣腿还是有扎实的基础,就不会 ...

  2. mysql从零开始之MySQL LIKE 子句

    MySQL LIKE 子句 我们知道在 MySQL 中使用 SQL SELECT 命令来读取数据, 同时我们可以在 SELECT 语句中使用 WHERE 子句来获取指定的记录. WHERE 子句中可以 ...

  3. dg create datafile auto failed 排除处理

    1.Environment:11.2.0.4 dg 2.Symptoms:告警描述:Wed Sep 22 14:29:15 2021Errors in file /data/app/oracle/di ...

  4. Java编译运行环境讨论(复古但能加深对Java项目的理解)

    Java编译运行环境讨论(复古但能加深对Java项目的理解) 如今我们大多数情况都会使用IDE来进行Java项目的开发,而一个如今众多优秀的IDE已经能够帮助我们自动的部署并调试运行我们的Java程序 ...

  5. VS2013编译报错——error LNK2001: 无法解析的外部符号 __imp_PathMatchSpecA E:\CaffeProgram\3train_mnist(p)\3train_mnist\gflags.lib(gflags.obj) 3train_mnist

    解决方案来自http://blog.csdn.net/yang6464158/article/details/41743641 感谢感谢~~

  6. 接口自动化-Python3+request上传文件,发送multipart/form-data编码

    1.安装requests_toolbelt   pip install requests-toolbelt 2.发送文件中的数据 from requests_toolbelt import Multi ...

  7. PAT (Basic Level) Practice (中文)1014 福尔摩斯的约会 (20分)

    1014 福尔摩斯的约会 (20分) 带侦探福尔摩斯接到一张奇怪的字条:我们约会吧! 3485djDkxh4hhGE 2984akDfkkkkggEdsb s&hgsfdk d&Hys ...

  8. Java(33)IO流的介绍&字节流

    作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15228446.html 博客主页:https://www.cnblogs.com/testero ...

  9. FastAPI 学习之路(五十四)startup 和 shutdown

    我们在实际的开发中呢,总会遇到这样的场景,我们想在启动或者终止的时候,做一些事情,那么应该如何实现呢,其实也是很简单.fastapi提供了这样的操作. 那么我们看下具体是怎么实现的呢 app = Fa ...

  10. LeetCode:堆专题

    堆专题 参考了力扣加加对与堆专题的讲解,刷了些 leetcode 题,在此做一些记录,不然没几天就忘光光了 力扣加加-堆专题(上) 力扣加加-堆专题(下) 总结 优先队列 // 1.java中有优先队 ...