数据格式
前面我们已经学习了一些简单的基本类型,现在学习复合类型,复合类型主要包括了数组,指针,切片等。现在先来学习数组.
数组
如果要存储班级里所有学生的数学成绩,应该怎样存储呢?可能有同学说,通过定义变量来存储。但是,问题是班级有80个学生,那么要定义80个变量吗?
像以上情况,最好是通过数组的方式来存储。
所谓的数组:是指一系列同一类型数据的集合。
数组定义
数组定义也是通过var 关键字,后面是数组的名字a,长度是10,类型是整型。表示:数组a能够存储10个整型数字。也就是说,数组a的长度是10。
我们可以通过len( )函数测试数组的长度,如下所示:
当定义完成数组a后,就在内存中开辟了10个连续的存储空间,每个数据都存储在相应的空间内,数组中包含的每个数据被称为数组元素(element),一个数组包含的元素个数被称为数组的长度。
注意:数组的长度只能是常量。以下定义是错误的:
var n int = 10
var a [n]int
数组赋值
数组定义完成后,可以对数组进行赋值操作。
数组是通过下标来进行操作的,下标的范围是从0开始到数组长度减1的位置。
var a[10] int 表示的范围是a[0],a[1],a[2]…,a[9]
完成对数组赋值的第一种方法:
如果现在给a[10]=29, 会出现什么情况呢?
但是这种赋值方式比较麻烦,所以可以使用第二种赋值方式,如下所示:
通过for循环完成数组的赋值与输出。注意:循环的条件,如果将循环条件修改成i<=10是否正确
在上一节中,我们说过可以通过len( )函数来获取 数组的长度,所以也可以对上面的程序,进行如下的修改:
对数组中的数据输出,也可以使用range.如下所示:
i变量存储的是数组的下标,data变量存储的是数组中的值。
如果只想输出数组中的元素值,不希望输出下标,可以使用匿名变量
上面的案例中,首先完成了数组的赋值,然后再输出数组中的值。但是,如果定义完成数组后,没有赋值,直接输出会出现什么样的问题呢?
a数组中的元素类型是整型,定义完成后,直接输出,结果全部是0.
当然数组中存储的元素类型也可以是其它类型,如下所示:
var a [10]float64//如果不赋值,直接输出,结果默认全部是0
vara[10]string//如果不赋值,直接输出,结果默认全部是空字符
var a [10]bool//如果不赋值,直接输出,结果默认全部是false.
数组初始化
上一小节中,首先先定义数组,然后再完成数组的赋值。其实,在定义数组时,也可以完成赋值,这种情况叫做数组的初始化。
具体案例如下:
冒泡排序
如何对数组中存储的数据,按照从大到小,或者从小到大进行排序?可以使用冒泡排序。
分析过程:
随机数
在GO语言中,产生随机数的语法如下:
要产生随机数,还需要导入“math/rand”包,
rand.Seed(123),设置随机种子,由于随机种子是固定的,所以运行该程序,发现每次生成的随机数是一样的。
所谓的随机种子:随机数是由随机种子根据一定的计算方法计算出来的数值。所以,只要计算方法一定,随机种子一定,那么产生的随机数就不会变。
为了解决这个问题,可以让随机种子随时发生变化,所以可以利用时间来作为随机种子。 如果用时间来做随机种子,需要导入“time”包。
rand.Int( );每次产生的整型随机数都非常的大,所以可以限制范围,使用的是rand中的Intn( )方法。例如:rand.Intn(100),限制100内的随机数。
最终程序可以进行如下的修改:
切片
切片概念
在讲解切片(slice)之前,大家思考一下数组有什么问题?
第一:数组定义完,长度是固定的。
例如:
定义的num数组长度是5,表示只能存储5个整型数字,现在向数组num中追加一个数字,这时会出错。
第二:使用数组作为函数参数进行传递时,如果实参为5个元素的整型数组,那么形参也必须5个元素的整型数组,否则出错。
针对以上两个问题,可以使用切片来进行解决。
切片:切片与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大,所以可以将切片理解成“动态数组”,但是,它不是数组。
切片与数组区别
通过定义,来比较一下切片与数组的区别
(1) 先回顾数组的基本定义初始化:
a:=[5]int{ }
数组中[ ]是一个固定的数字,表示长度。定义完后,长度是固定,最多存储5个数字。
(2)切片的基本定义初始化如下:
s:=[ ]int{ }//定义空切片
看定义的方式,发现与数组很相似,但是注意:切片中的[ ]是空的,或者是“…”.切片的长度和容量可以不固定。
现在通过程序演示,动态向切片中追加数据
append( )函数,第一个参数表示向哪个切片追加数据,后面表示具体追加的数据。
切片其它定义方式
(3)切片第二种定义方式:
var s1 []int //声明切片和声明数组一样,只是少了长度,此为空(nil)切片
(4)切片第三中定义方式,通过make( )函数实现
//借助make函数, 格式 make(切片类型, 长度, 容量)
s := make([]int, 5, 10)
什么是切片的长度与容量?
长度是已经初始化的空间(以上切片s初始空间默认值都是0)。容量是已经开辟的空间,包括已经初始化的空间和空闲的空间。
我们可以通过如下图来理解切片的长度与容量:
该切片的长度是5(存有数据,注意如果没有赋值,默认值都是0),容量是10,只不过有5个空闲区域。
即使没有给切片s赋值,初始化的空间(长度)默认存储的数据都是0。
演示如下:
输出的结果是:
在使用make( )函数定义切片时,一定要注意,切片长度要小于容量,例如:
s := make([]int, 10, 5)是错误的。
make( )函数中的容量参数是可以省略掉的,如:
这时长度与容量是相等的,都是10.
GO语言提供了相应的函数来计算切片的长度与容量。
接下来给切片s赋值,可以通过下标的方式直接来进行赋值。如下所示:
也可以通过循环的方式来进行赋值。
在这里一定要注意,循环结束条件是小于切片的长度,而不是容量。因为,切片的长度是指的是初始化的空间。以下方式会出现异常错误。
给切片赋完值后,怎样将切片中的数据打印出来呢?
第一种方式:直接通过下标的方式输出,例如:s[0],s[1]…。
第二种方式:通过循环的方式,注意循环结束的条件,也是小于切片的长度,如下所示:
或者使用range方式输出:
切片截取
上一小节中,已经完成了切片的定义,赋值等操作,接下来看一下关于切片的其它操作。首先说一下切片的截取操作,所谓截取就是从切片中获取指定的数据。
我们通过如下程序给大家解释一下:
以上程序输出结果:
s[0:3:5]是什么意思呢?
我们可以使用s[low:high:max]来表示
第一个数(low)表示下标的起点(从该位置开始截取),如果low取值为0表示从第一个元素开始截取,也就是对应的切片s中的1
第二个数(high)表示取到哪结束,也就是下标的终点(不包含该位置),3表示取出下标是0,1,2的数据(1,2,3),不包括下标为3的数据,那么也就是说取出的数据长度是3. 可以根据公式:3-0 计算(len=high-low),也就是第二个数减去第一个数,差就是数据长度。在这里可以将长度理解成取出的数据的个数。
第三个数用来计算容量,所谓容量:是指切片目前可容纳的最多元素个数。通过公式5-0计算(cap=max-low),也就是第三个数据减去第一个数。该案例中容量为5
关于切片的截取还有其它的操作,如下图所示:
接下来说,思考如下题,定义一个切片array,然后对该切片array进行截取操作(范围自定义),得到新的切片s6, 并修改切片s6某个元素的值。代码如下:
输出结果:
发现切片s中的值也发生了变化,也就是修改切片slice的值会影响到原切片s的值,在这里重点要理解的是:slice := array[1:4],将array切片中的s[1],s[2],s[3]截取作为新切片slice,实际上是切片slice指向了原切片s(在这里并不是为切片s新建一块区域)。所以修改s6,也会影响到array.
append
在第一节中,已经给大家讲解过切片与数组很大的一个区别就是:切片的长度是不固定的,可以向已经定义的切片中追加数据。并且也给大家简单的演示过通过append的函数,在原切片的末尾添加元素。
问题:可能有同学会问,如果容量不够用了,该怎么办呢?
例如有以下切片:
s:= make([]int, 5, 8)
定义了切片s,长度是5,容量是8
结果是:
并且前面我们讲解过,长度是指已经初始化的空间,现在切片s没有赋值,但是默认值为0
验证如下所示:
结果是:
现在开始通过append函数追加数据,如下所示:
结果是:
通过以上结果分析,发现是2倍的容量进行扩容。
但是我们修改一下循环条件看一下结果,将循环结束的条件修改的大一些,如下所示:
对应的结果:
通过以上的运行结果分析:当容量小于1024时是按照2倍容量扩容,当大于等于1024是不是按照2倍容量扩容。
copy函数
针对切片操作常用的方法除了append( )方法以外,还有copy方法.
基本语法:copy(切片1,切片2)
将第二个切片里面的元素,拷贝到第一个切片中。
下面通过一个案例,看一下该方法的使用:
上面案例中,将srcSlice中的元素拷贝到destSlice切片中。结果如下:
通过以上结果可以分析出,直接将srcSlice切片中两个元素拷贝到dstSlice元素中相同的位置。而dstSlice原有的元素备替换掉。
下面将以上程序修改一下,如下所示:
以上程序的结果是:
通过以上两个程序得出如下结论:在进行拷贝时,拷贝的长度为两个slice中长度较小的长度值。
切片作为函数参数
切片也可以作为函数参数,那么与数组作为函数参数有什么区别呢?
接下来通过一个案例,演示一下切片作为函数参数。
通过以上案例,发现在主函数main( )中,定义了一个切片s,然后调用InitData( )函数,将切片s作为实参传递到该函数中,并在InitData( )函数中完成初始化,该函数并没有返回值,但是在主函数中直接打印切片s,发现能够输出对应的值。也就是在InitData( )函数中对形参切片num赋值,影响到了main( )函数中的切片s.
但是,大家仔细想一下,如果我们这里传递参数不是切片,而是数组,那么能否完成该操作呢?
那么我们将上面的程序,修改成以数组作为参数进行传递的形式:
发现以数组的形式作为参数,并不能完成我们的要求,所以切片作为函数实参与数组作为函数实参,进行传递时,传递的方式是不一样的。
在GO语言中,数组作为参数进行传递是值传递,而切片作为参数进行传递是引用传递。
什么是值传递?什么是引用传递?
值传递:方法调用时,实参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值
引用传递:也称为传地址。函数调用时,实际参数的引用(地址,而不是参数的值)被传递给函数中相对应的形式参数(实参与形参指向了同一块存储区域),在函数执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数的值。
字符串处理函数
我们从文件中将数据读取出来以后,很多情况下并不是直接将数据打印出来,而是要做相应的处理。例如:去掉空格等一些特殊的符号,对一些内容进行替换等。
这里就涉及到对一些字符串的处理。在对字符串进行处理时,需要借助于包“strings”
下面讲解一下常用的字符串处理函数:
(1)Contains
(2)Join
结果如下:
(3)Index
结果:
(4)Repeat
结果:
(5)Replace
func Replace(s,old,newstring,nint)string
功能:在s字符串中,把old字符串替换为new字符串,n表示替换的次数,小于0表示全部替换
结果:
(6)Split
func Split(s,sepstring)[]string
功能:把s字符串按照sep分割,返回slice
结果:
(7)Trim
结果:
(8)Fields
func Fields(s string)[]string
功能:去除s字符串的空格符,并且按照空格分割返回slice
结果:
字符串转换
通过以上的讲解,发现字符串的处理是非常重要的,GO语言也提供了字符串与其它类型之间相互转换的函数。相应的字符串转换函数都在”strconv”包。
(1)Format
Format 系列函数把其他类型的转换为字符串。
将整型转换成字符串:
(2)Parse
Parse 系列函数把字符串转换为其他类型
(3)Append
Append 系列函数将整数等转换为字符串后,添加到现有的字节切片中。
结果
Map
GO语言中的字典结构是有键和值构成的。
所谓的键,就类似于新华字典的部首或拼音,可以快速查询出对应的数据。
如下图所示:
通过该图,发现某个键(key)都对应的一个值(value),如果现在要查询某个值,直接根据键就可以查询出某个值。
在这里需要注意的就是字典中的键是不允许重复的,就像身份证号一样。
字典结构定义
map[keyType]valueType
定义字典结构使用map关键字,[ ]中指定的是键(key)的类型,后面紧跟着的是值的类型。
键的类型,必须是支持==和!=操作符的类型,切片、函数以及包含切片的结构类型不能作为字典的键,使用这些类型会造成编译错误:
下面定义一个字典m,键的类型是整型,值的类型是字符串。
定义完后,直接打印,结果为空(nil)。
注意:字典中不能使用cap函数,只能使用len( )函数。len( )函数返回map拥有的键值对的数量
以上代码值为0,也就是没有值。
当然也可以使用make( )函数来定义,如下所示:
以上代码值为0,也就是没有值。
当然也可以指定容量。
输出的len值还是0,因为这里并没有赋值。
接下来可以给字典m2进行赋值,并且指定容量,如果容量不够自动扩容。
可以直接使用键完成赋值,再次强调键是唯一的,同时发现字典m2的输出结果,不一定是按照赋值的顺序输出的,每次运行输出的顺序可能都不一样,所以这里一定要注意:map是无序的,我们无法决定它的返回顺序,所以,每次打印结果的顺利有可能不同。
map 也可以定义完成后直接进行初始化
打印字典中的值
(1)可以直接通过键输出,如下所示:
(2)通过循环遍历的方式输出
输出的顺序是无序的。
(3)在输出的时候,还可以进行判断
第一个返回值为key所对应的value, 第二个返回值为key是否存在的条件,存在ok为true。
(3)删除map中的某个元素。
根据map中的键,删除对应的元素,也是非常的方便。
如下所示:
(4)map 作为函数参数是引用传递。
结构体
现在有一个需求,要求存储学生的详细信息,例如,学生的学号,学生的姓名,年龄,家庭住址等。按照以前学习的存储方式,可以以如下的方式进行存储:
通过定义变量的信息,进行存储。但是这种方式,比较麻烦,并且不利于数据的管理。
在GO语言中,我们可以通过结构体来存储以上类型的数据,结构体的定义如下:
type 后面跟着的是结构体的名字Student, struct表示定义的是一个结构体。
大括号中是结构体的成员,注意在定义结构体成员时,不要加var.
通过以上的定义,大家能够感觉出,通过结构体来定义复杂的数据结构,非常清晰。
结构体定义完成后,可以进行初始化。
(1)结构体初始化
注意:顺序初始化,每个成员必须初始化,在初始化时,值的顺序与结构体成员的顺序保持一致。
结构体定义完成后,结构体成员的使用。
(2)结构体比较与赋值
两个结构体可以使用 == 或 != 运算符进行比较,但不支持 > 或 <。
同类型的两个结构体变量可以相互赋值。
(3)结构体数组
上一小节,我们已经对结构体的定义,与基本使用有一定的了解了,下面有一个需求:用结构体存储多个学生的信息。
可以使用上一小节讲解的,通过结构体定义多个结构体变量,也可以定义结构体数组来存储。
结构体数组定义如下所示:
(4)结构体作为函数参数
结构体也可以作为函数参数,进行传递,如下所示:
结构体作为函数参数进行传递,是值传递。
指针
变量内存与地址
前面我们讲过存储数据的方式,可以通过变量,或者复合类型中的数组,切片,Map,结构体。
我们不管使用变量存储数据,还是使用符合类型存储数据,都有两层的含义:
存储的数据(内存),对应的地址。
接下来,通过变量来说明以上两个含义。例如,定义如下变量:
第一个Printf( )函数的输出,大家都很熟悉,输出变量i的值,这个实际上就是输出内存中存储的数据。在前面的课程中,已经讲解过,定义一个变量,就是在内存中开辟一个空间,用来存储数据,当给变量i赋值为100,其实就是将100存储在改空间内。
第二个Printf( )函数的输出,输出的是变量i在内存中的地址。通过如下图来给大家解释:
这张图,大家也应该非常熟悉,是在讲解变量时,画的一张图,0x100010假设是变量i的内存地址(通过第二个输出可以获取实际的地址),内存地址的作用:在输出变量中存储的数据时,是通过地址来找到该变量内存空间的。
这个内存地址和实际生活中的地址也很相似,例如:大家可以将内存空间想象成,我们上课的教室,教室中存放有学生,那么现在要找一个学生,必须要知道具体的地址以及教室门牌号。
以上程序输出的结果是:
指针变量
现在已经知道怎样获取变量在内存中的地址,但是如果想将获取的地址进行保存,应该怎样做呢?
可以通过指针变量来存储,所谓的指针变量:就是用来存储任何一个值的内存地址。
指针变量的定义如下:
指针变量p的定义是通过 这个符号来定义,指针变量p的类型为int, 表示存储的是一个整型变量的地址。
如果指针变量p存储的是一个字符串类型变量的地址,那么指针变量p的类型为string
p=&i :该行代码的意思是,将变量i的地址取出来,并且赋值给指针变量p.也就是指针变量p指向了变量i的存储单元。
可以通过如下图来表示:
在以上图中,一定要注意:指针变量p存储的是变量i的地址。
大家可以思考一个问题:
既然指针变量p指向了变量i的存储单元,那么是否可以通过指针变量p,来操作变量i中存储的数据?
答案是可以的,具体操作方式如下:
注意:在使用指针变量p来修改变量i的值的时候,前面一定要加上.(通过指针访问目标对象)
现在打印变量i的值已经有100变为20.
当然,也可以通过指针变量p来输出,变量i中的值,输出的方式如下所示:
所以,*p的作用就是根据存储的变量的地址,来操作变量的存储单元(包括输出变量存储单元中的值,和对值进行修改)
注意事项
在使用指针变量时,要注意以下两点。
1:默认值为nil
直接执行上面的程序,结果是:nil
2:不要操作没有合法指向的内存。
例如,在上面的案例中,我们定义了指针变量p,但是没有让指针变量指向任何一个变量,那么直接运行如下程序,会出现异常。
出现的错误信息如下:
所以,在使用指针变量时,一定要让指针变量有正确的指向。以下的操作是合法的:
在该案例中,定义了一个变量a, 同时定义了一个指针变量p, 将变量a的地址赋值给指针变量p,也就是指针变量p指向了变量a的存储单元。给指针变量p赋值,影响到了变量a.最终输出变量a中的值也是22.
new()函数
指针变量,除了以上介绍的指向以外(p=&a),还可以通过new( )函数来指向。
具体的应用方式如下:
new(int)作用就是创建一个整型大小的空间
然后让指针变量p指向了该空间,所以通过指针变量p进行赋值后,该空间中的值就是22.
new( )函数的作用就是C语言中的动态分配空间。但是在这里与C语言不同的地方,就是最后不需要关系该空间的释放。GO语言会自动释放。这也是比C语言使用方便的地方。
也可以使用自动推导类型的方式:
指针做函数参数
指针也可以作为函数参数,那么指针作为函数参数在进行传递的时候,是值传递还是引用传递呢?
大家都知道,普通变量作为函数参数进行传递是值传递,如下案例所示:
定义一个函数,实现两个变量值的交换。
通过以上案例,证实普通类型变量在传递时,为值传递。
那么使用指针作为函数参数呢?现在将以上案例修改成,用指针作为参数,如下所示:
通过以上案例证实,指针作为参数进行传递时,为引用传递,也就是传递的地址。
在调用Swap( )函数时,将变量a与变量b的地址传分别传递给指针变量num1,num2,这时num1和num2,分别指向了变量a,与变量b的内存存储单元,那么操作num1,num2实际上操作的就是变量a与变量b,所以变量a与变量b的值被交换。
数组指针
前面在讲解数组的时候,我们用数组作为函数参数,但是数组作为参数进行传递是值传递,如果想引用传递,可以使用数组指针。具体使用方式如下:
定义一个数组,作为函数Swap的实参进行传递,但是这里传递的是数组的地址,所以Swap的形参是数组指针。,
这时指针p,指向了数组a,对指针p的操作实际上是对数组a的操作,所以如果直接执行如下语句:fmt.Println(p),会输出数组a中的值。也可以通过p结合下标将对应的值取出来进行修改。最终在main函数中输出数组a,发现其元素也已经修改。
当然,我们也可以通过循环的方式来将数组指针中的数据打印出来:
指针数组
上一小节,讲解到的是数组指针,也就是让一个指针指向数组 ,然后可以通过该指针来操作数组。还有一个概念叫指针数组,这两个概念很容混淆,指针数组指的是一个数组中存储的都是指针(也就是地址)。也就是一个存储了地址的数组。
下面通过一个案例,看一下指针数组的应用
指针数组的定义方式,与数组指针定义方式是不一样的,注意指针数组是将“*”放在了下标的后面。
由于指针数组存储的都是地址,所以将变量i,与变量j的地址赋值给了指针数组p。
最后输出指针数组p中存储的地址。
思考:既然指针数组p存储了变量i 和变量j的的地址,那么怎样通过指针数组p操作变量i与变量j的值呢?
具体实现如下:
注意这里输出要注意的问题是,没有加小括号。(注意运算顺序)
当然,我们也可以通过for循环的方式来输出指针数组中对应的值。
切片指针
结果:
可以看到指针和切片名地址并不相同,因为切片名就是切片的地址,如下图所示:
因此,不能直接通过指针操作切片中的元素
切片指针作为函数参数:
结构体指针变量
我们前面定义了指针指向了数组,解决了数组引用传递的问题。那么指针是否可以指向结构体,也能够解决结构体引用传递的问题呢?完全可以。
下面我们先来看一下,结构体指针变量的定义:
也可以使用自动推导类型
现在定义了一个结构体指针变量,那么可以通过该指针变量来操作结构体中的成员项。
前面在讲解结构时,用结构体作为函数的参数,默认的是值传递,那么通过结构体指针,可以实现结构体的引用传递。具体实现的方式如下: