前言
在前面的篇章中,我们已经把基本数据类型都讲完,我们接下来的几个篇章将进入讲述跟容器有点类似的数据类型,还记的我们上一篇说的数组吗?我们上一篇把它比作是一个 凹槽容器,接下来我们要讲的就是 数组 这个类型,为了让我们更好的去学习和理解数组,我们先学习一下在学数组时需要使用到的知识。
(判断、循环、指针都是非常简单的东西,相比于前面的知识,简直就是小菜一碟)
判断(if)
格式:
if 条件 {
…代码
}
是不是很简单,就是当条件成立 时,就执行花括号中的代码,来我们直接上代码:
var a int8 = 1
var c int8 = 1
var b bool = false
if a == c {
fmt.Println("条件成立1")
}
if b {
fmt.Println("条件成立2")
}
/*
输出结果:
条件成立1
*/
我们变量 a 和 c 的值都是 1,所以使用 == 对比两个变量的值都是 1,所以条件成立,结果为 true,然后执行花括号中的代码,打印出 条件成立1。
而我们的变量 b 是 bool 布尔类型,并且值是 false,所以当 if 接收到 b 变量之后,判断值是 false,所以不会执行花括号里面的内容。
是不是很好理解?我在举个通俗的例子,我们做白日梦的时候:
如果,我中了5000万彩票,我就回家养猪。
- “如果” 对应的就是 if
- “我中了5000万” 这个假设对应我们的 条件
- “就回家养猪” 我们的条件满足(中了5000万),才能这么任性的辞职回家养猪
所以代码也是来源于生活的,我们平常还会这样说:
我买的彩票 如果,中了5000万,就回家养猪;如果,中了500万,就回家养鱼;都没中,就继续上班写代码吧。 用代码表达出来:
// 彩票中奖金额
var a int32 = 5000
if a == 5000 {
// 中了5000 万,养猪
fmt.Println("回家养猪")
} else if a == 500 {
// 中了 500 万,养鱼
fmt.Println("回家养鱼")
} else {
// 以上都没有中,秃头
fmt.Println("码代码搬砖秃头")
}
if a > 500 {
fmt.Println("我是单独的if")
}
/*
输出结果:
回家养猪
我是单独的if
*/
else 是关键字,用来串联条件,意思是 其它, else if 顾明思意就是 其它 的 判断,通俗的话来说就是 多个假设,我们代码里面叫 多条件,顺序从上往下进行判断,我们执行第一个判断 a == 5000,条件成立,所以 回家养猪。
这里要注意,多条件下(用 else 关键字串联起来),一旦判断条件成立,执行花括号内的代码后,不会再往下执行余下的判断。但 if a > 500 是独立的判断,与上面的一堆并无关联
最后面的 else { … } 又是什么意思呢?这个超简单,就是当前面的 if 条件都不成立时,就会执行 else { … } 的代码。(通俗的讲,就是 彩票没中5000万也没中500万,这个时候我们还是继续敲代码敲到秃头吧)
在后面的篇章中,我们会把学的知识都慢慢的运用起来,这里我们先把它理解。
循环
循环,这个更简单,更容易理解,都说代码来源于生活,生活中的循环就是重复的去做某件事;你在原地转十个圈 == 原地转圈的动作你循环了十次,这两个都是一个意思,可以理解吧? 是不是超级简单?既然理解了,接下来我们就讲讲该怎么让一段代码,重复执行10次。
我们需要在代码中使用 for 关键字
for 条件 { …代码 }
跟我们的 if 非常的像, 只要条件满足,就执行花括号中的代码; for 与 if 的不同点就在于 循环,只要 for 的条件成立,就会再次重复执行for,直到 条件不成立 才会结束执行。看代码:
// 创建变量a,赋值0
var a int32 = 0
// 判断a是否小于10
for a < 10 {
// 打印a变量的值(Print 不换行打印到控制台)
fmt.Print(a)
// a变量的值加1
a = a + 1
}
// for 执行完毕后继续往下执行代码
fmt.Println()
fmt.Println("for结束了,执行了我")
/*
输出结果:
0123456789
for结束了,执行了我
*/
是不是超级简单, 我们创建了一个 a 变量赋值0,然后执行 for 代码,判断 a 变量是否小于10,第一次执行 a 的值是0,所以条件成立,执行花括号中的代码打印a变量,再让a的值加1,花括号代码执行完毕;开始第二轮循环,继续判断a是否小于10,然后继续执行花括号…;直到 a 的值被加到了10,for 继续执行条判断 a 是否小于 10,条件不成立,所以不再执行 for 代码,进而代码往下执行 fro 或括号外的代码,输出 “for结束了,执行了我”。
非常简单是不是,就三个点:
- 创建一个变量
- 判断变量的值
- 修改变量的值
我们的 for 循环有一种简化的写法,同样可以达到一样的效果:
for 判断前执行; 条件判断; 花括号代码执行后执行 {…}
( a = a + 1 就是 a 变量自身加1, a++ 是它的简写,意思是一样的,自身加1; a-- 就是自身减1)
// 初始化变量a赋值0 | 条件判断a小于10 | {...} 花括号代码执行后a变量累加1
for a:=0; a < 10; a++ {
fmt.Print(a)
}
这个写法跟之前的 for 代码片段是一样的,只要理解了它的格式和执行顺序就超容易理解; for 1; 2; 3 {…} 格式分解:
- 1 在执行条件判断之前会执行该代码片段
- 2 判断条件
- 3 {…} 执行完花括号内的代码之后就会执行该代码片段
for true {
fmt.Println("我会一直输出这句话,知道内存不足程序崩掉")
}
各单位注意: 如果条件一直都是 true,就会一直循环操作,这样就会形成了死循环,就是出不来了,你在本地转圈转到死为止(直到程序内存溢出,程序就会卡死、崩掉)。
你看我们能不能做这样两个功能:
-
功能1: 循环10次,当 a 变量的值为 5 时,输出一句话“斗牛大陆”,并且不再执行本次循环的代码,立马开始下一轮循环。(通俗讲功能1:让你连续吃10顿饭,吃到第6顿的第一个菜时,剩下的菜都不吃了,立马开始吃第7顿饭。使用 continue)
-
功能2:循环10次,当 a 变量的值为 5 时,立马结束循环,后面的循环不再执行。(通俗讲功能2:让你连续吃10顿饭,吃到第6顿的第一个菜时,后面都不吃了,后面的第7、8、9、10顿都不吃了。使用 break)
上代码:
// a == 5 时,那顿饭剩下的菜就不吃了,立马开始下一顿
for a:=0; a < 10; a++ {
if a == 5 {
fmt.Print("斗牛大陆")
continue
}
fmt.Print(a)
}
/*
输出结果:
01234斗牛大陆6789
*/
// a == 5 时,剩下的菜会之后的每一顿饭都不吃了
for a:=0; a < 10; a++ {
if a == 5 {
break
}
fmt.Print(a)
}
/*
输出结果:
01234
*/
是不是超级简单,只需要使用两个关键字就能完成上面的两个功能:
- continue 跳出本次循环,立马执行下一轮循环
- break 跳出整个整个for循环,立马开始执行for循环外的代码。
我们这里对 for 的内容暂时告一段落,它还有一种用处,就是把 容器 内的元素一个个取出来,这里先不讲,我们下面讲数组的时候会用到。不知不觉已经讲了三种 for了,总结一下:
// 第一种
a := 0
for a < 10 {
fmt.Println(a)
a++
}
// 第二种
for b:=0; b<10; b++ {
fmt.Println(b)
}
// 第三种 死循环
for true {
fmt.Println("直到天荒地老")
}
指针(ptr)
指南针的那根针也叫指针,风水大师那罗盘上的跟针也是指针,指针的意思就是用来做指向的,指南针指向的是南边,风水大师的罗盘指向的也是南边,我们机械手表的时分秒针指向的是方向;所以指针的共同作用就是 指向。我们代码中的指针也是如此,也是指向的意思。
我们代码中的指针是用来指向内存的;之前篇章中,我说过程序运行的一切数据都是存储在内存中的,内存就像一个空间一个房间,我们的数据就像是房间里面的物品。既然这个指针是指向内存的,那我们先把内存简单的讲一下。
内存 (虚拟内存)
我们计算机中有一个叫内存条的配件,内存条上有很多个电容,电容通电代表这个电容存储的是1,电容不通电代表存储0,这是内存条的物理存储方式。cpu会把每个电容在内存条上的位置记录下来并存储在寄存器中,我们程序把数据存储进内存的时候,会先把数据转换成二进制(0和1组成的数字),然后cpu会根据数据的大小,分配对应数据大小的内存空间(一组电容的地址)给程序的数据,分别根据0和1给对应的电容进行通电。这就是CPU和内存条的关联,这个内存上的电容,我们可以称为物理内存。那 虚拟内存呢?这个超级好理解,就是CPU寄存器上存储的电容位置,我们把它称为虚拟内存(这里只做简单的结束,有兴趣的去自行学习)。内存指的就是所有电容,我们计算机存储数据时 CPU 把每8个电容的地址组织成一组,我们称之为字节,1个字节其实就是8个电容的地址组成的,所以我们说 1个byte 占 8个bit,指的就是这个意思。我们讲的这些跟指针又有什么关系呢?
var a byte = 1
我们创建一个类型为 byte 的 a 变量,赋值1(var a byte = 1),看看都发生了些什么:
- 程序创建变量 a ,并向系统申请一个存储 byte 类型数据的内存地址;
↓↓↓ - 系统通过cpu运算生成一个16进制数字,我们把这个16进制数字称为内存地址,cpu再次大发神威把8个还没被内存地址绑定的电容物理地址与这个内存地址进行绑定,他们之间的绑定关系我们也可以叫做 映射,就是把物理地址映射到这个内存地址,他们之间的绑定关系会缓存到寄存器中,然后系统把这个 内存地址(如 0xc00000a0a0)返回给我们的程序;
↓↓↓ - 程序拿到内存地址后会与 a 变量进行绑定,a → 地址(如 0xc00000a0a0) → 内存条电容位置,就是变量 a 指向 内存地址 指向 电容地址;
↓↓↓ - = 1 把数值1转换成一个 8bit 的二进制数 0000 0001,程序告诉系统把 0000 0001 这个数值存储到 0xc00000a0a0 这个地址中;
↓↓↓ - 系统通过cpu运算 把 0xc00000a0a0 虚拟内存地址映射的物理电容地址取出来,并给存储0位的电容断电表示0,给存储1位的电容通电表示1;
此时创建变量 a ,然后赋值 1 的步骤就做完了。
// a → 0xc00000a0a0 → 0000 0001
var a byte = 1
// a → 0xc00000a0a0 → 0000 0010
a = 2
此时我们给 a 变量再赋一个值 a = 2,这个时候会发生些什么呢?
-
我们的程序 把 0xc00000a0a0 和 0000 0010 传给系统
-
系统通过命令告诉CPU把 0000 0010 存储到 0xc00000a0a0 这个内存地址中,CPU一顿骚操作把映射的物理地址取出来,通过系统告诉硬件去把对应物理地址的电容分别进行断电和通电,让其分别表示0和1
a 赋值 2 就完成了;到此对数据存储和内存使用是不是有个大概认识了,实际中的计算机内存使用要比上面描述更加精彩,有兴趣的可以去自行了解,特别是我们的CPU、系统、内存、数据线、硬件控制等的原来,非常有趣。我们的内存讲解就到此为止了,准备进入我们的 指针 讲解。
内存的一点小知识(不感兴趣执行跳到下面的指针)
这里额外讲一下我们的string,如:
a := ‘天一子’
a = ‘天一子叮叮咚咚’
a 变量申请了一个 内存地址,并赋值 ‘天一子’,那么当我们再赋值 ‘天一子叮叮咚咚’ 的时候,a 的内存地址会发生改变吗?
答案是 不会,内存地址不变,但物理地址的映射会有编号,我们之前说过,一个中文汉字在UTF-8编码下占用3个字节,所以第一次赋值时,3个字符,cpu为这个内存地址分配了 3个字节 * 8bit * 3个电容地址;当再赋值 ‘天一子叮叮咚咚’ 后,由于 3 * 8 * 3 个电容地址装不下,需要 3 * 8 * 7 ,所以会为内存地址分配更多的电容地址。
那我们所说的 指针 又和这内存有什么关系呢?
当然有关系,我们上面说指南针的指针指向的是南方;而我们 Go 语言所说的指针,指向的就是 内存地址,我们的 指针类型 存储的值就是 内存地址;我们还可以使用 & + 变量名 就可以得到 给变量的内存地址了:
a := "天一子"
// 输出变量的值
fmt.Println(a)
// 输出变量的内存地址(16进制内存地址)
fmt.Println(&a)
/*
输出结果:
天一子
0xc0000881e0
*/
我们来创建一个 string指针类型,并赋值一个指针值:
a := "天一子"
// 创建一个 存放string类型内存地址的指针类型
var ptr *string = a&
fmt.Println(ptr)
/*
输出结果:
0xc00003a1f0
*/
var b int8 = 1
// 报错
ptr = &b
由上面可以看出,指针是有类型区分的,而且指针只能存储相同类型的指针值,如果在 int 类型指针变量中存储 string 类型指针值就会报错,所以同一类型指针只能存储同一类型的指针值。创建指针类型 只需要在数据类型前面使用 * 号即可;
上面我们说过,内存地址是一个16进制数,而我们的指针类型存储的值就是这个内存地址,这就意味着我们的 指针类型 其实就是一个数值类型,对应的就是我们的 int 类型,32位系统时对应int32,64位系统时对应int64,只是以16进制数表现出来,所以当你打印指针类型变量时,打印出来的就是一个16进制数,所以上面代码打印变量 ptr 时,输出的是16进制数 0xc00003a1f0。
我们通过 * + 指针变量名 就可以得到指针内存地址的值:
var a string = "天一子"
var ptr *string = &a
// 创建 b 变量并赋值 *pta(b变量和内存地址绑定,就是b变量指向a变量的内存地址)
b := *ptr
// 输出指针的值
fmt.Println(ptr)
/*
输出结果:
0xc00008a040
*/
// 输出指针所指向内存地址的值(此处输出a变量的值)
fmt.Println(b)
/*
输出结果:
天一子
*/
从上面代码我们得知,其实 变量a 和 变量b 都指向了 0xc00008a040内存地址;而 ptr变 的值存储的就是 0xc00008a040 的二进制值;现在我们对指针类型也理解了吧?那这个指针类型有什么用呢?复用内存地址:
var a = "aaa"
var b = &a
var c = a
fmt.Println("变量a的地址:", &a)
fmt.Println("变量b的地址:", b)
fmt.Println("变量c的地址:", &c)
fmt.Println(a)
*b = "bbb"
fmt.Println(a)
/*
输出结果:
变量a的地址: 0xc00003a1f0
变量b的地址: 0xc00003a1f0
变量c的地址: 0xc00003a200
aaa
bbb
*/
打印出来 a变量的内存地址 和 b变量指针值 都是一样的(0xc00003a1f0),而打印出来 c变量内存地址是0xc00003a200,由此我们得知 GO 语言的值传递其实是值复制; var c = a 程序把a变量的值提取出来,并向系统获取内存地址,系统通过cpu一顿骚操作拿到电容物理地址并进行通电断电,再把内存地址返回给程序,程序把新的内存地址和变量c进行映射;
*b = “bbb” 为指针指向的内存地址进行赋值,由于 b 变量指向的内存地址和 a变量 的内存地址都是一样的,所以 *b = “bbb” 更新这个内存地址的值时,a变量的值自然也是就变成了 “bbb”,所以最后变量a打印出来的就是 “bbb”。
指针的实际用途多用于结构体传递、数组切片等的场景使用,以后会讲述到。现在就讲述到这里吧,只要在这先对指针类型有个了解,至于以后的实际使用以后讲述到自然就明白了。我们给指针类型做个小总结:
- 指针类型只能存储同类型数据的指针(var ptr *string = &string)
- 符号 * + 数据类型 创建指针类型
- 符号 & + 变量 获取指针(ptr = &a)
- 符号 * + 变量 获取指针指向内存地址的值(a = *ptr)
- 指针类型存储的是一个16进制数的地址内存,所以 ptr = int
- 指针的零值是nil
- 最终总结:指针就是一个数值类型,值存储的16进制内存地址。
数组(array)
上一章讲字符串的时候说过,字符放在凹槽容器内;而我们接下来要讲的就是这个容器,我们把这种容器叫 数组。下面我们使用 [ ] 中括号创建一个占用3个byte空间的数组,:
// 使用中括号[],创建一个 byte 类型的数组,容量大小为3
var bArray [3]byte
// 变量名[下标] 给三个空间赋值
bArray[0] = 1
bArray[1] = 2
bArray[2] = 3
fmt.Println(bArray)
fmt.Println(&bArray[0])
fmt.Println(&bArray[1])
fmt.Println(&bArray[2])
/*
输出结果:
[1 2 3]
0xc0000a2058
0xc0000a2059
0xc0000a205a
*/
我们的 数组 如果是用来装byte的,就要声明数据类型,所以要在 [ ] 中括号后面添加数据类型, 数组创建时还需要明确容器的大小,所以中括号内需要传递一个数量,说明这个数组存放多少个元素; [3]string 表示数组可以存储3个字符串类型数据,[3]byte 表示数组可以存储3个byte类型数据。数组内的空间位置下标是 0 开始的,所以我们要往 bArray 的三个空间中存储值或取值时,只要通过 变量名+[下标] 就可以访问到数组空间。
其实只要我们记住几个点,数组就非常容易理解:
- 数组使用 [ ] 中括号创建
- 数组只能存储定义类型的数据类型
- 数组的空间大小 [数量] 定义后不可变
- 数组下标从 0 开始
- 变量名+[下标] 就能访问数组内对应的空间,我们把这个空间也叫做 元素
是不是超级简单?我们的数组还有木有其他的创建方式?如果我们 bArray[4] 下标4去取值会怎么样?
// 创建byte类型数组定义3个空间大小,并分别为每个空间赋值
var bArray [3]byte
bArray[0] = 1
bArray[1] = 2
bArray[2] = 3
fmt.Println(bArray)
// 创建数组时并同时为每个空间赋值
rArray := [2]rune{'钟', '离'}
fmt.Println(rArray)
// 访问 bArray数组下标4(报错:Invalid array index 4 (out of bounds for 3-element array))
fmt.Println(bArray[4])
数组创建常使用的两种方法,一种是先创建一个数组分批好内存空间,然后再给每个空间进行赋值;另一种是在创建数组的同时进行赋值。是不是看上去好像第二种比较好用和简单?这是要看使用场景的哦,如果你提前已经明确要存储的值,那么就直接使用第二种;如果你只知道要存储多少个值,并不确定要存储那些值,这种情况就需要使用第一种。
场景一:创建一个数组并存储四个状态值 生、老、病、死
var status = [4]rune{'生', '老', '病', '死'}
场景二:创建一个大小为1000的int32类型数组,并且给每个空间都赋值1~1000,如 a[0] = 1 给第一个下标0添加值1,依次添加到下标999值1000。如果这个场景由你来做,你会怎么做?
// 你不会这样做吧?
var bArray = [1000]int32{1, 2 ,...省略代码...,1000}
// 你不会这样做吧?
var bArray [1000]int32
bArray[0] = 1
bArray[1] = 2
...依次赋值 省略代码...
bArray[999] = 1000
// 我们使用上面学的for循环来完成这个场景,使用len函数获取数组长度1000
for int i = 0; i < len(bArray); i++ {
bArray[i] = i + 1
}
在实际项目中,我们经常会用到 for 循环来操作数组,for 循环还有一种用法经常使用(range 关键字):
// 把 bArray数组的元素循环赋值到变量a中
for a := range bArray {
fmt.Println(a)
}
使用 range 关键字,循环加载出 bArray 数组元素,for 会根据数组类型创建一个同样类型的变量 a,然后再把第一个元素赋值到该变量的内存地址上,下一次循环继续把元素赋值到这个 变量a的内存地址上;这个变量a只创建一次哦,每次循环只是复用内存地址,是不是很熟悉,你猜跟上面的指针有没有关系?有兴趣自己找答案。
文章写到这里,明白指针的用处木有?我之前讲过 go 语言中的值传递其实就是复制,所以 var a = b 会申请一个新的内存地址,并把b变量的值存储到a变量的内存地址中,如果不使用指针的方式复用内存地址的话,那么 for a := range bArray 就要为 a 变量申请1000个内存地址,但如果使用指针去实现的话,只需要用一个内存地址赋值1000次。这样是不是节省了很多内存空间?
我再举一个例子:
// 创建一个存储着两千字符的字典数组
func main() {
var runeArray = [2000]rune{'天','风'...省略1998个字符...}
printDictionaryRuneByIndex(runeArray, 0)
fmt.Println(runeArray[1])
fmt.Println(&runeArray[1])
}
// 打印数组指定下标元素
func printDictionaryRuneByIndex(runes [2000]rune, index int) {
fmt.Println(runes[index])
fmt.Println(&runes[index])
}
/*
输出结果:
风
0xc000098430
风
0xc000098470
*/
我们输出两个数组同一个下标的内容和地址,虽然输出的内容都是 “风” 当内存地址是不一样的;
我们的数组创建后,实则存储的是一组内存地址,如 var bArray [3]byte 实则= [byte内存地址1, byte内存地址2, byte内存地址3];当代码执行到 printDictionaryRuneByIndex函数 的时候,因为值传递是使用的是值复制,所以 runes变量 此时会先申请2000个内存地址,然后把 runeArray变量 中的值赋值到 runes变量中内存地址的值;这就是问题所在,2个数组变量 runeArray 和 runes 各占用了 2000个内存地址。如果我们使用指针,就可以复用数组了,不用再创建一个数组变量并申请2000个数组空间:
// 创建一个存储着两千字符的字典数组
func main() {
var runeArray = [2000]rune{'天','风'...省略1998个字符...}
printDictionaryRuneByIndex(&runeArray, 0)
fmt.Println(runeArray[1])
fmt.Println(&runeArray[1])
}
// 打印数组指定下标元素
func printDictionaryRuneByIndex(runes *[2000]rune, index int) {
fmt.Println(runes[index])
fmt.Println(&runes[index])
}
/*
输出结果:
风
0xc000004490
风
0xc000004490
*/
我们使用指针之后,不用额外再申请2000个内存地址并分配内存空间,我们只需要传递一个指针过去,这样可以大大的节省我们的内存开销,如果指针类型使用的好,还是在某些场景下可以提升一定的性能。要知道我们的内存是很宝贵的哦。
至此我们的 if判断、for循环、ptr指针、array数组讲述完毕。