前期回顾

前面的一章主要和大家分享了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. Python第3方模块安装

    前言: 我们在实际开发过程中,很多模块是Python没有自带的,所以我们使用模块前需要先安装第三方模块. 在线安装: 1.为了简便cmd指令操作,建议先打开Python目录下的Scripts文件夹下, ...

  2. FTP和TFTP

    文件传输协议 FTP概述: 文件传输协议FTP(File Transfer Protocol)[RFC 959]是互联网上使用最广泛的文件传输协议, FTP提供交互式的访问,允许用户知指明文件类型与格 ...

  3. Vue组件间的数据传输

    1.父组件向子组件传输数据:自定义属性 1 //父组件 2 <Son :msg="message" :user="userinfo"></So ...

  4. HTML选择器的权重(优先级)

    选择器的优先级主要用于样式发生冲突的情况下 选择器范围越小,优先级越高 行内样式>id选择器>类选择器>标签选择器>通用选择器 这里涉及一个权重值的问题,权重值越高,优先级越大 ...

  5. captcha_trainer 验证码识别-训练 使用记录

    captcha_trainer 验证码识别-训练 使用记录 在爬数据的时候,网站出现了验证码,那么我们就得去识别验证码了.目前有两种方案 接入打码平台(花钱,慢) 自己训练(费时,需要GPU环境,快) ...

  6. [转载]CSS3实现文本垂直排列

    最近的一个项目中要使文字垂直排列,也就是运用了CSS的writing-mode属性. writing-mode最初时ie中支持的一个属性,后来在CSS3中增添了这一新的属性,所以在ie中和其他浏览器中 ...

  7. jdbc简单学生管理系统

    这个是java连接mysql数据库的一个简单学生系统,通过jdbc连接数据库. 工具类 JDBCuntils. package Student; import java.io.IOException; ...

  8. python redis自带门神 lock 方法

    redis 支持的数据结构比较丰富,自制一个锁也很方便,所以极少提到其原生锁的方法.但是在单机版redis的使用时,自带锁的使用还是非常方便的.自己有车还打啥滴滴顺风车是吧,本篇主要介绍redis-p ...

  9. 8M的摄像头,30fps摄像时,60hz的LCD刷新频率,请问camera每秒向BB传输多少数据,如何计算

    8M的摄像头,30fps摄像时,60hz的LCD刷新频率,请问camera每秒向BB传输多少数据,如何计算 xiang2012 Post at 2012/8/7 10:37:33 8M的摄像头,30f ...

  10. stm32学习心得体会

    stm32作为现在嵌入式物联网单片机行业中经常要用多的技术,相信大家都有所接触,今天这篇就给大家详细的分析下有关于stm32的出口,还不是很清楚的朋友要注意看看了哦,在最后还会为大家分享有些关于stm ...