Go基础,2024年最新Golang通用流行框架大全

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注go)
img

正文

// 以下是错误的

s := make([]int, 10, 5)

make()函数中的容量参数是可以省略掉的,如:

s := make([]int,10)

这时长度与容量是相等的,都是10.

GO语言提供了相应的函数来计算切片的长度与容量,示例如下:

s := make([]int,5,10)

fmt.Println(“长度是”,len(s))

fmt.Println(“容量是”,cap(s))

接下来给切片s赋值,可以通过下标的方式直接来进行赋值。如下所示:

s := make([]int,5,10)

s[0] = 1

s[1] = 2

也可以通过循环的方式来进行赋值。

s := make([]int,5,10)

for i:=0;i<len(s) ;i++ {

s[i] = i

}

在这里一定要注意,循环结束条件是小于切片的长度,而不是容量。因为,切片的长度是指的是初始化的空间。以下方式会出现异常错误。

for i:=0;i<cap(s) ;i++ {

s[i] = i

}

给切片赋完值后,怎样将切片中的数据打印出来呢?

  1. 第一种方式:直接通过下标的方式输出,例如:s[0],s[1].....

  2. 第二种方式: 通过循环的方式,注意循环结束的条件,也是小于切片的长度,如下所示:

for i:=0;i<len(s) ;i++ {

fmt.Println(s[i])

}

或者使用range方式输出:

for _,v := range s {

fmt.Println(v)

}

7.2.5 切片截取

上一小节中,已经完成了切片的定义,赋值等操作,接下来看一下关于切片的其它操作。首先说一下切片的截取操作,所谓截取就是从切片中获取指定的数据。

我们通过如下程序给大家解释一下:

//定义切片 并且完成初始化

s := []int{10,20,30,0,0}

//从切片s中截取数据

slice := s[0:3:5]

fmt.Println(slice)

以上程序输出结果:

[10 20 30]

其中s[0:3:5]是什么意思呢?我们来解释一下。每个位置的数字为s[low:high:max]

  1. 第一个数low表示下标的起点(从该位置开始截取),如果low取值为0表示从第一个元素开始截取,也就是对应的切片s中的10。

  2. 第二个数high表示取到哪结束,也就是下标的终点(不包含该位置),3表示取出下标是0,1,2的数据(10,20,30),不包括下标为3的数据,那么也就是说取出的数据长度是3。可以根据公式:3-0计算(len=high-low),也就是第二个数减去第一个数,差就是数据长度。在这里可以将长度理解成取出的数据的个数。

  3. 第三个数用来计算容量,所谓容量:是指切片目前可容纳的最多元素个数。通过公式5-0计算(cap=max-low),也就是第三个数据减去第一个数。该案例中容量为5。

现在将以上程序进行修改:

//定义切片 并且完成初始化

s := []int{10,20,30,40,50}

//从切片s中截取数据

slice := s[0:3:5]

fmt.Println(slice)

结果是:

[10 20 30]

因为起点还是0,也就是10开始,终点还是3也就是到30结束.长度是3,容量是5。

继续修改该程序:

//定义切片 并且完成初始化

s := []int{10,20,30,40,50}

//从切片s中截取数据

slice := s[0:4:5]

fmt.Println(slice)

结果是:

[10 20 30 40]

因为起点还是0,也就是10开始,终点还是4也就是到40结束。长度是4,容量是5。

继续修改该程序

//定义切片 并且完成初始化

s := []int{10,20,30,40,50}

//从切片s中截取数据

slice := s[1:4:5]

fmt.Println(slice)

slice切片结果是:

[20 30 40]

那么容量是多少呢?容量为4,通过第三个数减去第一个数(5-1)计算。

通过画图的方式来表示slice切片中的容量。

在这里插入图片描述

通过上面的图,可以发现切片s经过截取操作以后,将结果赋值给切片slice后,长度是3,容量是4,只不过有一块区域是空闲的。

切片其他操作。

如下表所示:

| 操作 | 含义 |

| — | — |

| s[n] | 切片s中索引位置为n的项 |

| s[:] | 从切片s的索引位置0到len(s)-1处所获得的切片 |

| s[low:] | 从切片s的索引位置low到len(s)-1处所获得的切片 |

| s[:high] | 从切片s的索引位置0到high处所获得的切片,len=high |

| s[low:high] | 从切片s的索引位置low到high处所获得的切片,len=high-low |

| s[low:high:max] | 从切片s的索引位置low到high处所获得的切片,len=high-low,cap=max-low |

| len(s) | 切片s的长度,总是<=cap(s) |

| cap(s) | 切片s的容量,总是>=len(s) |

下面通过一个案例,演示一下。

  1. s[:]:

在这里插入图片描述结果是所有的值。

  1. s[low:]在这里插入图片描述结果是下标3后面的所有值。

  2. s[:high]

import18.png结果是6前面的值。

  1. s[low:high]

在这里插入图片描述

结果是2-5的值。array[2:5]表示从下标为2的元素(包含该元素)开始取,到下标为5的元素(不包含该元素)结束。所以切片s5的长度是3。切片s5的容量是多少呢?是8,根据array切片的容量是10,减去array[2:5]中的2。

以上就是关于切片的基本操作,这些操作在以后的开发过程中会经常用到,希望大家记住基本的规律。

7.2.6 思考题

接下来说,思考如下题,定义一个切片array,然后对该切片array进行截取操作(范围自定义),得到新的切片s6,并修改切片s6某个元素的值。代码如下:

在这里插入图片描述

s6切片的结果是:[2,3,4]因为是从下标为2的元素(包含)开始取,到下标为5的元素(不包含)结束,取出3个元素,也就是长度为3。

现在将程序进行如下修改:

在这里插入图片描述

现在程序的输出结果是:

s6 = [2 3 888]

因为切除了234,然后现在0是2,1是3,2是4,然后把s6[2]也就是s6[4]赋值为888

接下来输出切片array的值:

在这里插入图片描述

输出的结果如下:

s6 = [2 3 888]

array = [0 1 2 3 888 5 6 7 8 9]

发现切片array中的值也发生了变化,也就是修改切片s6的值会影响到原切片array的值,下面通过画图的形式来说明其原因。

在这里插入图片描述

在这里重点要理解的是:s6 := array[2:5],将array切片中的array[2]array[3]array[4]截取作为新切片s6,实际上是切片s6指向了原切片array(在这里并不是为切片s6新建一块区域)。所以修改s6,也会影响到array。

下面继续修改上面的程序:

在这里插入图片描述

以上程序中,切片s7的值是多少?

结果是:

s7 = [888 5 6 7 8]

下面也是通过画图的形式,来解释该程序的结果:

在这里插入图片描述

继续思考,现在在原有的程序中又加了一行,如下图所示:

在这里插入图片描述

最终,切片s7与原来切片array的值分别是多少?

结果所示:

s6 = [2 3 888]

s7 = [888 5 999 7 8]

array = [0 1 2 3 888 5 999 7 8 9]

7.2.7 append函数的使用

在第一节中,已经给大家讲解过切片与数组很大的一个区别就是:切片的长度是不固定的,可以向已经定义的切片中追加数据。并且也给大家简单的演示过通过append的函数,在原切片的末尾添加元素。

arr := []int{1,2,3}

arr = append(arr,4) //追加一个数

arr = append(arr,5,6,7) //追加多个数

fmt.Println(arr)

如果容量不够用了,该怎么办呢?

例如有以下切片:

s:= make([]int, 5, 8)

定义了切片s,长度是5,容量是8k

s := make([]int,5,8)

fmt.Printf(“len = %d,cap=%d\n”,len(s),cap(s))

结果是:

len = 5 cap = 8

并且前面我们讲解过,长度是指已经初始化的空间,现在切片s没有赋值,但是默认值为0

验证如下所示:

s := make([]int,5,8)

fmt.Printf(“len = %d,cap=%d\n”,len(s),cap(s))

fmt.Println(s)

结果是:

len = 5 cap = 8

[0 0 0 0 0]

现在开始通过append函数追加数据,如下所示:

s := make([]int,5,8)

s = append(s,1)

fmt.Println(s)

fmt.Printf(“len = %d,cap=%d\n”,len(s),cap(s))

输出结果是:

[0 0 0 0 0 1]

len = 6 cap = 8

从输出的结果上,我们完全能够体会到,append函数的作用是在末尾追加(直接在默认值后面追加数据),由于追加了一个元素,所以长度为6.

但是如果我们把程序修改成如下所示:

s := make([]int,5,8)

//s = append(s,1)

s[0] = 1

fmt.Println(s)

fmt.Printf(“len = %d,cap=%d\n”,len(s),cap(s))

输出结果是:

[1 0 0 0 0]

len = 5 cap = 8

由于s[0]=1是直接给下标为0的元素赋值,并不是追加,所以结果的长度不变。

下面我们继续通过append( )继续追加数据:

s := make([]int,5,8)

s = append(s,1)

s = append(s,2)

s = append(s,3)

fmt.Println(s)

fmt.Printf(“len = %d,cap=%d\n”,len(s),cap(s))

结果是:

[0 0 0 0 0 1 2 3]

len = 8 cap = 8

追加完成3个数据后,长度变为了8,与容量相同。

那么如果现在通过append( )函数,继续向切片s中继续追加一个数据,那么容量会变为多少呢?

代码如下:

s := make([]int,5,8)

s = append(s,1)

s = append(s,2)

s = append(s,3)

s = append(s,4)

fmt.Println(s)

fmt.Printf(“len = %d,cap=%d\n”,len(s),cap(s))

输出的结果是:

[0 0 0 0 0 1 2 3 4]

len = 9 cap = 16

追加完成一个数据后,长度变为9,大于创建切片s时的容量,所以切片s扩容,变为16.

那么切片的容量是否是以2倍容量来进行扩容的呢?

我们可以来验证一下:

import29.png

输出结果是:

import30.png

通过以上结果分析,发现是2倍的容量进行扩容。

但是我们修改一下循环条件看一下结果,将循环结束的条件修改的大一些,如下所示:

import31.png

对应的结果:

import32.png

通过以上的运行结果分析:当容量小于1024时是按照2倍容量扩容,当大于等于1024就不是按照2倍容量扩容。

7.2.8 copy函数使用

针对切片操作常用的方法除了append\()方法以外,还有copy方法。

基本语法:copy(切片1,切片2)

将第二个切片里面的元素,拷贝到第一个切片中。

下面通过一个案例,看一下该方法的使用:

import34.png

上面案例中,将srcSlice中的元素拷贝到destSlice切片中。结果如下:

dst = [1 2 6 6 6]

通过以上结果可以分析出,直接将srcSlice切片中两个元素拷贝到dstSlice元素中相同的位置。而dstSlice原有的元素备替换掉。

下面将以上程序修改一下,如下所示:

import35.png

以上程序的结果是:

src = [6 6]

通过以上两个程序得出如下结论:在进行拷贝时,拷贝的长度为两个slice中长度较小的长度值。

思考以下程序输出的结果:

import36.png

结果是:

slice2 = [1 2 3]

现在将程序进行如下修改:

import37.png

结果是:

slice1 = [5 4 3 4 5]

7.2.9 切片作为函数参数

切片也可以作为函数参数,那么与数组作为函数参数有什么区别呢?

接下来通过一个案例,演示一下切片作为函数参数。

在这里插入图片描述

通过以上案例,发现在主函数main()中,定义了一个切片s,然后调用InitData()函数,将切片s作为实参传递到该函数中,并在InitData()函数中完成初始化,该函数并没有返回值,但是在主函数中直接打印切片s,发现能够输出对应的值。也就是在InitData()函数中对形参切片num赋值,影响到了main()函数中的切片s

但是,大家仔细想一下,如果我们这里传递参数不是切片,而是数组,那么能否完成该操作呢?

那么我们将上面的程序,修改成以数组作为参数进行传递的形式:

在这里插入图片描述

发现以数组的形式作为参数,并不能完成我们的要求,所以切片作为函数实参与数组作为函数实参,进行传递时,传递的方式是不一样的。

在GO语言中,数组作为参数进行传递是值传递,而切片作为参数进行传递是引用传递。

7.2.10 值传递和引用传递:

  • 值传递:方法调用时,实参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值

  • 引用传递:也称为传地址。函数调用时,实际参数的引用(地址,而不是参数的值)被传递给函数中相对应的形式参数(实参与形参指向了同一块存储区域),在函数执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数的值。

建议:以后开发中使用切片来代替数组。

7.3 Map


前面我们学习了GO语言中数组,切片类型,但是我们发现使用数组或者是切片存储的数据量如果比较大,那么通过下标来取出某个具体的数据的时候相对来说,比较麻烦。例如:

names := []string{“张三”,“李四”,“王五”}

fmt.Println(names[2])

现在要取出切片中存储的“王五”,那么需要数一下对应的下标值是多少,这样相对来说就比较麻烦。有没有一种结构能够帮我们快速的取出数据呢?就是字典结构。

说道字典大家想到的就是:

import666.png

在使用新华字典查询某个字,我们一般都是根据前面的部首或者是拼音来确定出要查询的该字在什么位置,然后打开对应的页码,查看该字的解释。

GO语言中的字典结构是有键和值构成的。

所谓的键,就类似于新华字典的部首或拼音,可以快速查询出对应的数据。

如下图所示:

import22.png

通过该图,发现某个键(key)都对应的一个值(value),如果现在要查询某个值,直接根据键就可以查询出某个值。

在这里需要注意的就是字典中的键是不允许重复的,就像身份证号一样。

7.3.1 字典结构定义

map[keyType]valueType

定义字典结构使用map关键字,[ ]中指定的是键(key)的类型,后面紧跟着的是值的类型。

键的类型,必须是支持==!=操作符的类型,切片、函数以及包含切片的结构类型不能作为字典的键,使用这些类型会造成编译错误:

//err invalid map key type []string

dict := map[[]string]int{}

下面定义一个字典m,键的类型是整型,值的类型是字符串。

var m map[int]string

fmt.Println(m)

定义完后,直接打印,结果为空nil

注意:字典中不能使用cap函数,只能使用len()函数。len()函数返回map拥有的键值对的数量

var m map[int]string

fmt.Println(len(m))

以上代码值为0,也就是没有值。

当然也可以使用make()函数来定义,如下所示:

m2 := make(map[int]string)

fmt.Println(m2)

fmt.Println(len(m2))

以上代码值为0,也就是没有值。

当然也可以指定容量。

m2 := make(map[int]string,3)

fmt.Println(m2)

fmt.Println(len(m2))

输出的len值还是0,因为这里并没有赋值。

接下来可以给字典m2进行赋值,并且指定容量,如果容量不够自动扩容。

m2 := make(map[int]string,3)

m2[1] = “张三”

m2[2] = “李四”

m2[3] = “王五”

fmt.Println(m2)

fmt.Println(len(m2))

可以直接使用键完成赋值,再次强调键是唯一的,同时发现字典m2的输出结果,不一定是按照赋值的顺序输出的,每次运行输出的顺序可能都不一样,所以这里一定要注意:map是无序的,我们无法决定它的返回顺序,所以,每次打印结果的顺利有可能不同。

map也可以定义完成后直接进行初始化

m4 := map[int]string{1:“make”,2:“Go”}

fmt.Println(m4[1])

fmt.Println(m4[2])

也就是在定义的同时给他直接赋值,然后打印出来

7.3.2 打印字典中的值

  1. 可以直接通过键输出,如下所示:

m4 := map[int]string{1:“make”,2:“Go”}

fmt.Println(m4[1])//make

fmt.Println(m4[2])//go

通过打印键的方式就能得到值

  1. 通过循环遍历的方式输出

m4 := map[int]string{1:“make”,2:“Go”}

for key,value := range m4 {

fmt.Println(key)

fmt.Println(value)

}

//1 make

//2 go

其中key代表的是键,value代表的是值

输出的顺序是无序的。

  1. 在输出的时候,还可以进行判断。

m4 := map[int]string{1:“make”,2:“Go”}

value,ok := m4[1]

if ok == true{

fmt.Println(value)

}else{

fmt.Println(“key不存在”)

}

第一个返回值为key所对应的value, 第二个返回值为key是否存在的条件,存在ok为true。

删除map中的某个元素。

根据map中的键,删除对应的元素,也是非常的方便。

如下所示:

m4 := map[int]string{1:“make”,2:“Go”}

delete(m4,1) //删除key为1的内容

fmt.Println(m4)//2 go

map作为函数参数是引用传递。

func test(m map[int]string){

delete(m,1)

}

func main(){

m4 := map[int]string{1:“make”,2:“Go”}

test(m4)

fmt.Println(m4)// 2 go

}

第一个Test定义了一个删除键为1的方法,然后在main里面又重新初始化了一个map然后调用Test的方法,最后输出的结果就是go,因为调用Test方法的时候把键为1的键值删除了。

7.4 结构体


现在有一个需求,要求存储学生的详细信息,例如,学生的学号,学生的姓名,年龄,家庭住址等。按照以前学习的存储方式,可以以如下的方式进行存储:

import41.png

通过定义变量的信息,进行存储。但是这种方式,比较麻烦,并且不利于数据的管理。

在GO语言中,我们可以通过结构体来存储以上类型的数据,结构体的定义如下:

import42.png

type后面跟着的是结构体的名字Student, struct表示定义的是一个结构体。

大括号中是结构体的成员,注意在定义结构体成员时,不要加var。通过以上的定义,大家能够感觉出,通过结构体来定义复杂的数据结构,非常清晰。

结构体定义完成后,可以进行初始化。

7.4.1 结构体初始

import43.png

注意:顺序初始化,每个成员必须初始化,在初始化时,值的顺序与结构体成员的顺序保持一致。

import44.png

结构体定义完成后,结构体成员的使用。

import45.png

7.4.2 结构体比较与赋值

两个结构体可以使用 == 或 != 运算符进行比较,但不支持 > 或 <。

import46.png

同类型的两个结构体变量可以相互赋值。

import47.png

7.4.3 结构体数组

上一小节,我们已经对结构体的定义,与基本使用有一定的了解了,下面有一个需求:用结构体存储多个学生的信息。

可以使用上一小节讲解的,通过结构体定义多个结构体变量,也可以定义结构体数组来存储。

结构体数组定义如下所示:

import48.png

上面的代码首先是定义了一个Student的结构体,然后在main方法里面用一个变量Students接收了这个新建的[]Student结构体数组,结构体是放一个Student那么,结构体数组就可想而知了,是放多个结构体,然后循环遍历Students的下标也就是有几个结构体就会遍历几次,最后再打印里面的sutdents里面的name,最后输出的结果"张三",“李四”,“王五”。

7.4.4 结构体作为函数参数

结构体也可以作为函数参数,进行传递,如下所示:

在这里插入图片描述

把结构体作为参数的话,参数类型就只能放相对应的结构体,不然会报错。

上面代码首先在Test里面修改一下student里面的id为666,然后在main里面新建一个为s的结构体,这个结构体和student一样,所以Test里面能放,放进去之后就会修改这个id,最后打印的结果为:id:666,name:mike,sex:m,age:18,addr:bj

结构体作为函数参数进行传递,是值传递。

7.5 指针


7.5.1 变量内存与地址

前面我们讲过存储数据的方式,可以通过变量,或者复合类型中的数组、切片、Map、结构体。我们不管使用变量存储数据,还是使用符合类型存储数据,都有两层的含义:存储的数据(内存),对应的地址。

接下来,通过变量来说明以上两个含义。例如,定义如下变量:

import70.png

第一个Printf()函数的输出,大家都很熟悉,输出变量i的值,这个实际上就是输出内存中存储的数据。在前面的章节中,已经讲解过,定义一个变量,就是在内存中开辟一个空间,用来存储数据,当给变量i赋值为100,其实就是将100存储在改空间内。

第二个Printf()函数的输出,输出的是变量i在内存中的地址。通过如下图来给大家解释:

import71.png

这张图,大家也应该非常熟悉,是在讲解变量时,画的一张图,0x100010假设是变量i的内存地址(通过第二个输出可以获取实际的地址),内存地址的作用:在输出变量中存储的数据时,是通过地址来找到该变量内存空间的。

这个内存地址和实际生活中的地址也很相似,例如:大家可以将内存空间想象成,我们上课的教室,教室中存放有学生,那么现在要找一个学生,必须要知道具体的地址以及教室门牌号。

以上程序输出的结果是:

import72.png

7.5.2 指针变量

现在已经知道怎样获取变量在内存中的地址,但是如果想将获取的地址进行保存,应该怎样做呢?

可以通过指针变量来存储,所谓的指针变量:就是用来存储任何一个值的内存地址。

指针变量的定义如下:

import74.png

指针变量p的定义是通过*这个符号来定义,指针变量p的类型为*int,表示存储的是一个整型变量的地址。

如果指针变量p存储的是一个字符串类型变量的地址,那么指针变量p的类型为*string p=&i该行代码的意思是,将变量i的地址取出来,并且赋值给指针变量p。也就是指针变量p指向了变量i的存储单元。

可以通过如下图来表示:

import76.png

在以上图中,一定要注意:指针变量p存储的是变量i的地址。

大家可以思考一个问题:

既然指针变量p指向了变量i的存储单元,那么是否可以通过指针变量p,来操作变量i中存储的数据?

答案是可以的,具体操作方式如下:

在这里插入图片描述

注意:在使用指针变量p来修改变量i的值的时候,前面一定要加上*(通过指针访问目标对象)

现在打印变量i的值已经有100变为80.

当然,也可以通过指针变量p来输出,变量i中的值,输出的方式如下所示:

import77.png

所以,*p的作用就是根据存储的变量的地址,来操作变量的存储单元(包括输出变量存储单元中的值,和对值进行修改)

7.5.3 注意事项

在使用指针变量时,要注意以下两点。

  1. 默认值为nil

var p *int

fmt.Println§

直接执行上面的程序,结果是:nil

  1. 不要操作没有合法指向的内存。

例如,在上面的案例中,我们定义了指针变量p,但是没有让指针变量指向任何一个变量,那么直接运行如下程序,会出现异常。

var p *int

*p = 99 //没有指向 直接操作

fmt.Println§

出现的错误信息如下:

import78.png

所以,在使用指针变量时,一定要让指针变量有正确的指向。以下的操作是合法的:

var a int

var p *int

p = &a //指向变量a

*p = 99

fmt.Println§

在该案例中,定义了一个变量a,同时定义了一个指针变量p,将变量a的地址赋值给指针变量p,也就是指针变量p指向了变量a的存储单元。给指针变量p赋值,影响到了变量a。最终输出变量a中的值也是56

7.5.4 new()函数

指针变量,除了以上介绍的指向以外(p=&a),还可以通过new()函数来指向。

具体的应用方式如下:

var p *int

p = new(int)

*p = 59

fmt.Println(*p)

new(int)作用就是创建一个整型大小(4字节)的空间

然后让指针变量p指向了该空间,所以通过指针变量p进行赋值后,该空间中的值就是57。

new()函数的作用就是C语言中的动态分配空间。但是在这里与C语言不同的地方,就是最后不需要关系该空间的释放。GO语言会自动释放。这也是比C语言使用方便的地方。

也可以使用自动推导类型的方式:

q := new(int)

*q = 77

fmt.Println(*q)

7.5.5 指针做函数参数

指针也可以作为函数参数,那么指针作为函数参数在进行传递的时候,是值传递还是引用传递呢?

大家都知道,普通变量作为函数参数进行传递是值传递,如下案例所示:

定义一个函数,实现两个变量值的交换。

import79.png

通过以上案例,证实普通类型变量在传递时,为值传递。

那么使用指针作为函数参数呢?现在将以上案例修改成,用指针作为参数,如下所示:

import80.png

通过以上案例证实,指针作为参数进行传递时,为引用传递,也就是传递的地址。

在调用Swap()函数时,将变量a与变量b的地址传分别传递给指针变量num1num2,这时num1num2,分别指向了变量a,与变量b的内存存储单元,那么操作num1num2实际上操作的就是变量a与变量b,所以变量a与变量b的值被交换。

7.5.6 数组指针

前面在讲解数组的时候,我们用数组作为函数参数,但是数组作为参数进行传递是值传递,如果想引用传递,可以使用数组指针。具体使用方式如下:

import81.png

定义一个数组,作为函数Swap的实参进行传递,但是这里传递的是数组的地址,所以Swap的形参是数组指针。

这时指针p,指向了数组a,对指针p的操作实际上是对数组a的操作,所以如果直接执行如下语句:fmt.Println(*p),会输出数组a中的值。也可以通过*p结合下标将对应的值取出来进行修改。最终在main函数中输出数组a,发现其元素也已经修改。

当然,我们也可以通过循环的方式来将数组指针中的数据打印出来:

import82.png

7.5.7 指针数组

上一小节,讲解到的是数组指针,也就是让一个指针指向数组,然后可以通过该指针来操作数组。还有一个概念叫指针数组,这两个概念很容混淆,指针数组指的是一个数组中存储的都是指针(也就是地址)。也就是一个存储了地址的数组。

下面通过一个案例,看一下指针数组的应用

import83.png

指针数组的定义方式,与数组指针定义方式是不一样的,注意指针数组是将“*”放在了下标的后面。

由于指针数组存储的都是地址,所以将变量i,与变量j的地址赋值给了指针数组p。

最后输出指针数组p中存储的地址。

思考:既然指针数组p存储了变量i和变量j的的地址,那么怎样通过指针数组p操作变量i与变量j的值呢?

具体实现如下:

import84.png

注意这里输出要注意的问题是,没有加小括号。(注意运算顺序)

当然,我们也可以通过for循环的方式来输出指针数组中对应的值。

import85.png

7.5.8 结构体指针变量

我们前面定义了指针指向了数组,解决了数组引用传递的问题。那么指针是否可以指向结构体,也能够解决结构体引用传递的问题呢?完全可以。

下面我们先来看一下,结构体指针变量的定义:

import86.png

也可以使用自动推导类型

import87.png

现在定义了一个结构体指针变量,那么可以通过该指针变量来操作结构体中的成员项。

import88.png

前面在讲解结构时,用结构体作为函数的参数,默认的是值传递,那么通过结构体指针,可以实现结构体的引用传递。具体实现的方式如下:

import89.png

八、面向对象

=================================================================

8.1 面向对象


前面我们已经将GO语言中各种类型,给大家讲解完毕了,那么接下来要给大家讲解的是面向对象编程思想。

在讲解具体面向对象编程之前,先说一下面向过程编程。我们前面学习都是面向过程的一种编程思想,接下来可以从生活中理解面向过程:

import07.png

如果我们自己来修电脑,应该有哪些步骤呢?

  • 第一步:判断问题的原因

  • 第二步:找工具

  • 第三步:暴力拆卸

这个修理的步骤就是面向过程,所谓的面向过程就是:强调的是步骤、过程、每一步都是自己亲自去实现的。

如果采用面向对象的思想,那么应该怎样修电脑呢?

import08.png

找维修店的工作人员来帮我们修电脑,但是到底怎么修,我们是不用考虑的,也就是说我们不关心步骤与过程。

大家可以想一下,在生活中还有哪些事情是面向过程,面向对象的。

比如说,做饭,面向过程就是自己做,自己买菜,自己洗,自己炒,整个过程都有自己来完成,但是如果是面向对象,可以叫外卖,不用关心饭是怎么做的。

所以通过以上案例,大家能够体会出,面向过程就是强调的步骤,过程,而面向对象强调的是对象,找个人来做。

在面向对象中,还有两个概念是比较重要的,一是对象,二是类。

什么是对象

万物皆对象,例如小明同学是一个对象,小亮同学也是一个对象。那么我们在生活中怎样描述一个对象呢?

比如,描述一下小明同学:

姓名:小明

性别:男

身高:180cm

体重:70kg

年龄:22岁

吃喝拉撒睡一切正常健康,吃喝嫖赌抽。

通过以上的描述,可以总结出在生活中描述对象,可以通过特征(身高,体重,年龄等)和行为(爱好等)来进行描述。

那么在程序中,可以通过属性和方法(函数)来描述对象。属性就是特征,方法(函数)就是行为。所以说,对象必须具有属性和方法。虽然说,万物皆对象,但是在描述一个对象的时候,一定要具体不能泛指,例如,不能说“电灯”是一个对象,而是说具体的哪一台“电灯”。

大家可以思考一下,如果我们现在描述一下教室中某一台电灯,应该有哪些属性(特征)和方法(行为)呢?

下面我们在思考一下,下面这道题:

小明(一个学生)\杨老师\邻居王叔叔\小亮的爸爸\小亮的妈妈

找出这道题中所有对象的共性(所谓共性,指的是相同的属性和方法)。

所以说,我们可以将这些具有相同属性和相同方法的对象进行进一步的封装,抽象出来类这个概念。

类就是个模子,确定了对象应该具有的属性和方法。

对象是根据类创建出来的

例如:上面的案例中,我们可以抽出一个“人”类(都有年龄,性别,姓名等属性,都有吃饭,走路等行为),“小明”这个对象就是根据“人”类创建出来的,也就是说先有类后有对象。

GO语言中的面向对象

前面我们了解了一下,什么是面向对象,以及类和对象的概念。但是,GO语言中的面向对象在某些概念上和其它的编程语言还是有差别的。

严格意义上说,GO语言中没有类(class)的概念,但是我们可以将结构体比作为类,因为在结构体中可以添加属性(成员),方法(函数)。

面向对象编程的好处比较多,我们先来说一下“继承”,

所谓继承指的是,我们可能会在一些类(结构体)中,写一些重复的成员,我们可以将这些重复的成员,单独的封装到一个类(结构体)中,作为这些类的父类(结构体),我们可以通过如下图来理解:

import09.png

当然严格意义上,GO语言中是没有继承的,但是我们可以通过”匿名组合”来实现继承的效果。

8.2 匿名函数


8.2.1 匿名字段

一般情况下,定义结构体的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。

当匿名字段也是一个结构体的时候,那么这个结构体所拥有的全部字段都被隐式地引入了当前定义的这个结构体。

//人

type Person struct {

name string

sex byte

age int

}

//学生

type Student struct {

Person //匿名字段,那么默认Student就包含了Person的所有字段

id int

addr string

}

Person也就是上面定义的这个Person结构体。

8.2.2 初始化

//人

type Person struct {

name string

sex byte

age int

}

//学生

type Student struct {

Person//匿名字段,那么默认Student就包含了Person的所有字段

id int

addr string

}

func main() {

//顺序初始化

s1 := Student{Person{“mike”,‘m’,18},1,“sz”}

//s1 = {Person:{name:mike sex:109 age:18}id:1 addr:sz}

fmt.Printf(“s1=%+v\n”,s1)

//s2 := Student{“mike”,‘m’,18,1,“sz”}//err

//部分成员初始化1

s3 := Student{Person:Person{“lily”,‘f’,19},id:2}

//s3 = {Person:{name:lily sex:102 age:19}id:2 addr:}

fmt.Printf(“s3=%+v\n”,s3)

//部分成员初始化2

s4 := Student{Person:Person{name:“tom”},id:3}

//s4 = {Person:{name:tomsex:0age:0}id:3addr:}

fmt.Printf(“s4=%+v\n”,s4)

}

然后我们在main里面调用Student就能直接对Person里面的属性赋值。

8.2.3 成员的操作

var s1 Student//变量声明

//给成员赋值

s1.name = “mike”//等价于s1.Person.name=“mike”

s1.sex = ‘m’

s1.age = 18

s1.id = 1

s1.addr = “sz”

fmt.Println(s1) //{{mike 109 18}1 sz}

var s2 Student//变量声明

s2.Person = Person{“lily”,‘f’,19}

s2.id = 2

s2.addr = “bj”

fmt.Println(s2) //{{lily 102 19}2 bj}

或者我们声明一个Student的变量也能调用它里面的属性。

8.2.4 同名字段

//人

type Person struct{

name string

sex byte

age int

}

//学生

type Student struct{

Person //匿名字段,那么默认Student就包含了Person的所有字段

id int

addr string

name string //和Person中的name同名

}

func main(){

var s Student//变量声明

//给Student的name,还是给Person赋值?

s.name = “mike”

//{Person:{name:sex:0age:0}id:0addr:name:mike}

fmt.Printf(“%+v\n”,s)

//默认只会给最外层的成员赋值

//给匿名同名成员赋值,需要显示调用

s.Person.name = “yoyo”

//Person:{name:yoyosex:0age:0}id:0addr:name:mike}

fmt.Printf(“%+v\n”,s)

}

如果命名重名的话我们调用只会给最外层的使用,也就是Student,如果说你要给Person赋值的话得明确表示。s.Person.name="张三"

8.2.5 其它匿名字段

  1. 非结构体类型

所有的内置类型和自定义类型都是可以作为匿名字段的:

type mystr string//自定义类型

type Person struct {

name string

sex byte

age int

}

type Student struct {

Person //匿名字段,结构体类型

int //匿名字段,内置类型

mystr //匿名字段,自定义类型

}

func main() {

//初始化

s1 := Student{Person{“mike”,‘m’,18},1,“bj”}

//{Person:{name:mikesex:109age:18}int:1mystr:bj}

fmt.Printf(“%+v\n”,s1)

//成员的操作,打印结果:mike,m,18,1,bj

fmt.Printf(“%s,%c,%d,%d,%s\n”,s1.name,s1.sex,s1.age,s1.int,s1.mystr)

}

不一样要结构体才能作为匿名字段,其实定义一个类型也是一样的。

  1. 结构体指针类型

type Person struct { //人

name string

sex byte

age int

}

type Student struct {//学生

*Person //匿名字段,结构体指针类型

id int

addr string

}

func main() {

//初始化

s1 := Student{&Person{“mike”,‘m’,18},1,“bj”}

//{Person:0xc0420023e0id:1addr:bj}

fmt.Printf(“%+v\n”,s1)

//mike,m,18

fmt.Printf(“%s,%c,%d\n”,s1.name,s1.sex,s1.age)

//声明变量

var s2 Student

s2.Person = new(Person)//分配空间

s2.name = “yoyo”

s2.sex = ‘f’

s2.age = 20

s2.id = 2

s2.addr = “sz”

//yoyo10220220

fmt.Println(s2.name,s2.sex,s2.age,s2.id,s2.age)

}

在匿名方法里面也是能使用指针的,只要在前面加上&就行。

8.3 方法


8.3.1 概述

在面向对象编程中,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些函数,这种带有接收者的函数,我们称为方法(method)。本质上,一个方法则是一个和特殊类型关联的函数。

一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。

在Go语言中,可以给任意自定义类型(包括内置类型,但不包括指针类型)添加相应的方法。

⽅法总是绑定对象实例,并隐式将实例作为第⼀实参 (receiver),方法的语法如下:

func (receiver ReceiverType) funcName (parameters) (results)

  • 参数 receiver 可任意命名。如⽅法中未曾使⽤,可省略参数名。

  • 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接⼝或指针。

  • 不支持重载方法,也就是说,不能定义名字相同但是不同参数的方法。

8.3.2 为类型添加方法

  1. 基础类型作为接收者

// 自定义类型,给int改名为MyInt

type MyInt int

// 在函数定义时,在其名字之前放上一个变量,即是一个方法

func (a MyInt) Add(b MyInt) MyInt {

return a + b

}

//传统方式的定义

func Add(a, b MyInt) MyInt {//面向过程

return a + b

}

func main() {

var a MyInt=1

var b MyInt=1

//调用func (aMyInt) Add(bMyInt)

fmt.Println(“a.Add(b)=”,a.Add(b))//a.Add(b)=2

//调用func Add(a,bMyInt)

fmt.Println(“Add(a,b)=”,Add(a,b))//Add(a,b)=2

}

通过上面的例子可以看出,面向对象只是换了一种语法形式来表达。方法是函数的语法糖,因为receiver其实就是方法所接收的第1个参数。

注意:虽然方法的名字一模一样,但是如果接收者不一样,那么方法就不一样。

  1. 结构体作为接收者

方法里面可以访问接收者的字段,调用方法通过点.访问,就像`struct``里面访问字段一样:

type Person struct {

name string

sex byte

age int

}

func (p Person) PrintInfo(){//给Person添加方法

fmt.Println(p.name,p.sex,p.age)

}

func main() {

p:=Person{“mike”,‘m’,18}//初始化

p.PrintInfo()//调用func(pPerson)PrintInfo()

}

打印结果为mike,m,18,你方法写的是Person那么这个方法只能传Person,不能传别的类型。

8.3.3 值语义和引用语义

type Person struct {

name string

sex byte

age int

}

// 指针作为接收者,引用语义

func (p *Person) SetInfoPointer(){

// 给成员赋值

(*p).name = “yoyo”

p.sex = ‘f’

p.age = 22

}

// 值作为接收者,值语义

func (p Person) SetInfoValue(){

// 给成员赋值

p.name = “yoyo”

p.sex = ‘f’

p.age = 22

}

func main() {

// 指针作为接收者,引用语义

p1 := Person{“mike”,‘m’,18} // 初始化

fmt.Println(“函数调用前=”,p1) // 函数调用前={mike10918}

(&p1).SetInfoPointer()

fmt.Println(“函数调用后=”,p1) // 函数调用后={yoyo10222}

fmt.Println(“==========================”)

p2 := Person{“mike”,‘m’,18} // 初始化

// 值作为接收者,值语义

fmt.Println(“函数调用前=”,p2) // 函数调用前={mike10918}

p2.SetInfoValue()

fmt.Println(“函数调用后=”,p2) // 函数调用后={mike10918}

}

8.3.4 方法集

类型的方法集是指可以被该类型的值调用的所有方法的集合。

用实例实例valuepointer调用方法(含匿名字段)不受⽅法集约束,编译器编总是查找全部方法,并自动转换receiver实参。

  1. 类型*T方法集

一个指向自定义类型的值的指针,它的方法集由该类型定义的所有方法组成,无论这些方法接受的是一个值还是一个指针。

如果在指针上调用一个接受值的方法,Go语言会聪明地将该指针解引用,并将指针所指的底层值作为方法的接收者。

类型*T⽅法集包含全部receiver T + *T⽅法:

type Person struct{

name string

sex byte

age int

}

// 指针作为接收者,引用语义

func (p *Person) SetInfoPointer(){

(*p).name=“yoyo”

p.sex=‘f’

p.age=22

}

// 值作为接收者,值语义

func (p Person) SetInfoValue(){

p.name=“xxx”

p.sex=‘m’

p.age=33

}

func main() {

// p为指针类型

var p*Person = &Person{“mike”,‘m’,18}

p.SetInfoPointer() // func §SetInfoPointer()

p.SetInfoValue() // func (*p)SetInfoValue()

(*p).SetInfoValue() // func (*p)SetInfoValue()

}

  1. 类型T方法集

一个自定义类型值的方法集则由为该类型定义的接收者类型为值类型的方法组成,但是不包含那些接收者类型为指针的方法。

但这种限制通常并不像这里所说的那样,因为如果我们只有一个值,仍然可以调用一个接收者为指针类型的方法,这可以借助于Go语言传值的地址能力实现。

package main

import “fmt”

type Student struct {

name string

age int

}

// 指针作为接收者 引用语义

func (s *Student) SetStuPointer() {

s.name = “Bob”

s.age = 18

}

// 值作为接收者 值语义

func (s Student) SetStuValue() {

s.name = “Peter”

s.age = 18

}

func main() {

// 指针作为接收者,引用语义

s1 := Student{“Miller”, 18} // 初始化

fmt.Println("函数调用前 = ", s1) // 函数调用前 = {Miller 18}

(&s1).SetStuPointer()

fmt.Println("函数调用后 = ", s1) // 函数调用后 = {Bob 18}

fmt.Println(“==========================”)

s2 := Student{“mike”, 18} // 初始化

//值 作为接收者,值语义

fmt.Println("函数调用前 = ", s2) // 函数调用前 = {mike 18}

s2.SetStuValue()

fmt.Println("函数调用后 = ", s2) // 函数调用后 = {mike 18}

}

// 总结 : (引用语义:会改变结构体内容) (值语义:不会改变结构体内容)

五、 匿名字段

  1. 方法的继承

如果匿名字段实现了一个方法,那么包含这个匿名字段的struct也能调用该方法。

type Person struct {

name string

sex byte

age int

}

//Person定义了方法

func (p *Person) PrintInfo() {

fmt.Printf(“%s,%c,%d\n”,p.name,p.sex,p.age)

}

type Student struct {

Person//匿名字段,那么Student包含了Person的所有字段

id int

addr string

}

func main() {

p := Person{“mike”,‘m’,18}

p.PrintInfo()

s := Student{Person{“yoyo”,‘f’,20},2,“sz”}

s.PrintInfo()

}

也就是说我用student继承了person那么我就拥有了person的一切不管是字段,还是方法,我都能调用。

  1. 方法的重写

type Person struct {

name string

sex byte

age int

}

//Person定义了方法

func (p *Person) PrintInfo() {

fmt.Printf(“Person:%s,%c,%d\n”,p.name,p.sex,p.age)

}

type Student struct {

Person//匿名字段,那么Student包含了Person的所有字段

id int

addr string

}

//Student定义了方法

func (s *Student) PrintInfo() {

fmt.Printf(“Student:%s,%c,%d\n”,s.name,s.sex,s.age)

}

func main() {

p:=Person{“mike”,‘m’,18}

p.PrintInfo() //Person:mike,m,18

s:=Student{Person{“yoyo”,‘f’,20},2,“sz”}

s.PrintInfo() //Student:yoyo,f,20

s.Person.PrintInfo() //Person:yoyo,f,20

}

也就是说我调用了Person的方法,但是我觉得这个方法不行,然后我自己又重新写了个方法,最后调用student方法的时候就只会调用我这个方法,而不会调用person的方法了

六、 方法值和方法表达式

类似于我们可以对函数进行赋值和传递一样,方法也可以进行赋值和传递。

根据调用者不同,方法分为两种表现形式:方法值和方法表达式。两者都可像普通函数那样赋值和传参,区别在于方法值绑定实例,⽽方法表达式则须显式传参。

  1. 方法值

type Person struct{

name string

sex byte

age int

}

func (p *Person) PrintInfoPointer() {

fmt.Printf(“%p,%v\n”,p,p)

}

func (p Person) PrintInfoValue(){

fmt.Printf(“%p,%v\n”,&p,p)

}

//上面是定义的方法

func main() {

p:=Person{“mike”,‘m’,18}

p.PrintInfoPointer() //0xc0420023e0,&{mike 109 18}

pFunc1:=p.PrintInfoPointer //方法值,隐式传递 receiver

pFunc1() //0xc0420023e0,&{mike 109 18}

pFunc2:=p.PrintInfoValue

pFunc2() //0xc042048420,{mike 109 18}

}

  1. 方法表达式

type Person struct {

name string

sex byte

age int

}

func (p *Person) PrintInfoPointer() {

fmt.Printf(“%p,%v\n”,p,p)

}

func (p Person) PrintInfoValue() {

fmt.Printf(“%p,%v\n”,&p,p)

}

//上面是定义的方法

func main() {

p:=Person{“mike”,‘m’,18}

p.PrintInfoPointer()//0xc0420023e0,&{mike 109 18}

//方法表达式,须显式传参

//func pFunc1 (p *Person))

pFunc1:=(*Person).PrintInfoPointer

pFunc1(&p) //0xc0420023e0,&{mike 109 18}

pFunc2:=Person.PrintInfoValue

pFunc2§ //0xc042002460,{mike 109 18}

}

8.3.5 匿名字段

  1. 方法的继承

如果匿名字段实现了一个方法,那么包含这个匿名字段的struct也能调用该方法。

type Person struct {

name string

sex byte

age int

}

//Person定义了方法

func (p *Person) PrintInfo() {

fmt.Printf(“%s,%c,%d\n”,p.name,p.sex,p.age)

}

type Student struct {

Person//匿名字段,那么Student包含了Person的所有字段

id int

addr string

}

func main() {

p := Person{“mike”,‘m’,18}

p.PrintInfo()

s := Student{Person{“yoyo”,‘f’,20},2,“sz”}

s.PrintInfo()

}

也就是说我用student继承了person那么我就拥有了person的一切不管是字段,还是方法,我都能调用。

  1. 方法的重写

type Person struct {

name string

sex byte

age int

}

//Person定义了方法

func (p *Person) PrintInfo() {

fmt.Printf(“Person:%s,%c,%d\n”,p.name,p.sex,p.age)

}

type Student struct {

Person//匿名字段,那么Student包含了Person的所有字段

id int

addr string

}

//Student定义了方法

func (s *Student) PrintInfo() {

fmt.Printf(“Student:%s,%c,%d\n”,s.name,s.sex,s.age)

}

func main() {

p:=Person{“mike”,‘m’,18}

p.PrintInfo() //Person:mike,m,18

s:=Student{Person{“yoyo”,‘f’,20},2,“sz”}

s.PrintInfo() //Student:yoyo,f,20

s.Person.PrintInfo() //Person:yoyo,f,20

}

也就是说我调用了Person的方法,但是我觉得这个方法不行,然后我自己又重新写了个方法,最后调用student方法的时候就只会调用我这个方法,而不会调用person的方法了

8.3.6 方法值和方法表达式

类似于我们可以对函数进行赋值和传递一样,方法也可以进行赋值和传递。

根据调用者不同,方法分为两种表现形式:方法值和方法表达式。两者都可像普通函数那样赋值和传参,区别在于方法值绑定实例,⽽方法表达式则须显式传参。

  1. 方法值

type Person struct{

name string

sex byte

age int

}

func (p *Person) PrintInfoPointer() {

fmt.Printf(“%p,%v\n”,p,p)

}

func (p Person) PrintInfoValue(){

fmt.Printf(“%p,%v\n”,&p,p)

}

//上面是定义的方法

func main() {

p:=Person{“mike”,‘m’,18}

p.PrintInfoPointer() //0xc0420023e0,&{mike 109 18}

pFunc1:=p.PrintInfoPointer //方法值,隐式传递 receiver

pFunc1() //0xc0420023e0,&{mike 109 18}

pFunc2:=p.PrintInfoValue

pFunc2() //0xc042048420,{mike 109 18}

}

  1. 方法表达式

type Person struct {

name string

sex byte

age int

}

func (p *Person) PrintInfoPointer() {

fmt.Printf(“%p,%v\n”,p,p)

}

func (p Person) PrintInfoValue() {

fmt.Printf(“%p,%v\n”,&p,p)

}

//上面是定义的方法

func main() {

p:=Person{“mike”,‘m’,18}

p.PrintInfoPointer()//0xc0420023e0,&{mike 109 18}

//方法表达式,须显式传参

//func pFunc1 (p *Person))

pFunc1:=(*Person).PrintInfoPointer

pFunc1(&p) //0xc0420023e0,&{mike 109 18}

pFunc2:=Person.PrintInfoValue

pFunc2§ //0xc042002460,{mike 109 18}

}

8.4 多态与接口


在讲解具体的接口之前,先看如下问题。

使用面向对象的方式,设计一个加减的计算器

代码如下:

package main

import “fmt”

//父类,这是结构体

type Operate struct {

num1 int

num2 int

}

//加法子类,这是结构体

type Add struct {

Operate

}

//减法子类,这是结构体

type Sub struct {

Operate

}

//加法子类的方法

func (a *Add) Result() int {

return a.num1 + a.num2

}

可以看到ADD里面是用父类结构体的,然后直接返回num1+num2就行了

//减法子类的方法

func (s *Sub) Result() int {

return s.num1 - s.num2

}

可以看到Sub里面是用父类结构体的,然后直接返回num1-num2就行了

//方法调用

func main0201() {

//创建加法对象

//var a Add

//a.num1 = 10

//a.num2 = 20

//v := a.Result()

//fmt.Println(v)

//可以看到调用起来还是很简单的,直接给父类结构体的属性赋值,然后调用加法的方法就行。

//创建减法对象

var s Sub

s.num1 = 10

s.num2 = 20

v := s.Result()

fmt.Println(v)

}

//可以看到调用起来还是很简单的,直接给父类结构体的属性赋值,然后调用减法的方法就行

以上实现非常简单,但是有个问题,在main()函数中,当我们想使用减法操作时,创建减法类的对象,调用其对应的减法的方法。但是,有一天,系统需求发生了变化,要求使用加法,不再使用减法,那么需要对main()函数中的代码,做大量的修改。将原有的代码注释掉,创建加法的类对象,调用其对应的加法的方法。有没有一种方法,让main()函数,只修改很少的代码就可以解决该问题呢?有,要用到接下来给大家讲解的接口的知识点。

8.4.1 什么是接口

接口就是一种规范与标准,在生活中经常见接口,例如:笔记本电脑的USB接口,可以将任何厂商生产的鼠标与键盘,与电脑进行链接。为什么呢?原因就是,USB接口将规范和标准制定好后,各个生产厂商可以按照该标准生产鼠标和键盘就可以了。

在程序开发中,接口只是规定了要做哪些事情,干什么。具体怎么做,接口是不管的。这和生活中接口的案例也很相似,例如:USB接口,只是规定了标准,但是不关心具体鼠标与键盘是怎样按照标准生产的.

在企业开发中,如果一个项目比较庞大,那么就需要一个能理清所有业务的架构师来定义一些主要的接口,这些接口告诉开发人员你需要实现那些功能。

8.4.2 接口定义

接口定义的语法如下:

//先定义接口 一般以er结尾 根据接口实现功能

type Humaner interface {

//方法 方法的声明

sayhi()

}

怎样具体实现接口中定义的方法呢?

//Student的结构体

type student11 struct {

name string

age int

score int

}

//Student的打印方法

func (s *student11)sayhi() {

fmt.Printf(“大家好,我是%s,今年%d岁,我的成绩%d分\n”,s.name,s.age,s.score)

}

//teacher11的结构体

type teacher11 struct {

name string

age int

subject string

}

//teacher11的方法

func (t *teacher11)sayhi() {

fmt.Printf(“大家好,我是%s,今年%d岁,我的学科是%s\n”,t.name,t.age,t.subject)

}

具体的调用如下:

func main() {

//接口是一种数据类型 可以接收满足对象的信息

//接口是虚的 方法是实的

//接口定义规则 方法实现规则

//接口定义的规则 在方法中必须有定义的实现

var h Humaner

stu := student11{“小明”,18,98}

//stu.sayhi()

//将对象信息赋值给接口类型变量

h = &stu

h.sayhi()

//直接将Student的对象赋值给了h接口,然后就能实现方法的调用

tea := teacher11{“老王”,28,“物理”}

//tea.sayhi()

//将对象赋值给接口 必须满足接口中的方法的声明格式

h = &tea

h.sayhi()

}

只要类(结构体)实现对应的接口,那么根据该类创建的对象,可以赋值给对应的接口类型。

接口的命名习惯以er结尾。

8.4.3 多态

接口有什么好处呢?实现多态。

多态就是同一个接口,使用不同的实例而执行不同操作

所谓多态指的是多种表现形式,如下图所示:

import13.png

使用接口实现多态的方式如下:

package main

import “fmt”

//先定义接口 一般以er结尾 根据接口实现功能

type Humaner1 interface {

//方法 方法的声明

sayhi()

}

//student12的结构体

type student12 struct {

name string

age int

score int

}

//student12的方法

func (s *student12)sayhi() {

fmt.Printf(“大家好,我是%s,今年%d岁,我的成绩%d分\n”,s.name,s.age,s.score)

}

//teacher12的结构体

type teacher12 struct {

name string

age int

subject string

}

//teacher12的方法

func (t *teacher12)sayhi() {

fmt.Printf(“大家好,我是%s,今年%d岁,我的学科是%s\n”,t.name,t.age,t.subject)

}

//多态的实现

//将接口作为函数参数 实现多态

func sayhello(h Humaner1) {

h.sayhi()

}

func main() {

stu := student12{“小明”,18,98}

//调用多态函数

sayhello(&stu)

tea := teacher12{“老王”,28,“Go”}

sayhello(&tea)

}

关于接口的定义,以及使用接口实现多态,大家都比较熟悉了,但是多态有什么好处呢?现在还是以开始提出的计算器案例给大家讲解一下。

8.4.4 多态案例

使用多态的功能,实现一个加减计算器。完整代码如下:

package main

import “fmt”

//定义接口

type Opter interface {

//方法声明

Result() int

}

//父类

type Operate struct {

num1 int

num2 int

}

//加法子类

type Add struct {

Operate

}

//加法子类的方法

func (a *Add) Result() int {

return a.num1 + a.num2

}

//减法子类

type Sub struct {

Operate

}

//减法子类的方法

func (s *Sub) Result() int {

return s.num1 - s.num2

}

//创建一个类负责对象创建

//工厂类

type Factory struct {

}

func (f *Factory) Result(num1 int, num2 int, ch string) {

switch ch {

case “+”:

var a Add

a.num1 = num1

a.num2 = num2

Result(&a)

case “-”:

var s Sub

s.num1 = num1

s.num2 = num2

Result(&s)

}

}

//通过设计模式调用

func main() {

//创建工厂对象

var f Factory

f.Result(10, 20, “+”)

}

8.4.5 接口继承与转换(了解)

接口也可以实现继承:

package main

import “fmt”

//先定义接口 一般以er结尾 根据接口实现功能

type Humaner2 interface { //子集

//方法 方法的声明

sayhi()

}

type Personer interface { //超集

Humaner2 //继承sayhi()

sing(string)

}

type student13 struct {

name string

age int

score int

}

func (s *student13)sayhi() {

fmt.Printf(“大家好,我是%s,今年%d岁,我的成绩%d分\n”,s.name,s.age,s.score)

}

func (s *student13)sing(name string) {

fmt.Println(“我为大家唱首歌”,name)

}

func main() {

//接口类型变量定义

var h Humaner2

var stu student13 = student13{“小吴”,18,59}

h = &stu

h.sayhi()

//接口类型变量定义

var p Personer

p = &stu

p.sayhi()

p.sing(“大碗面”)

}

接口继承后,可以实现“超集”接口转换“子集”接口,代码如下:

package main

import “fmt”

//先定义接口 一般以er结尾 根据接口实现功能

type Humaner2 interface { //子集

//方法 方法的声明

sayhi()

}

type Personer interface { //超集

Humaner2 //继承sayhi()

sing(string)

}

type student13 struct {

name string

age int

score int

}

func (s *student13)sayhi() {

fmt.Printf(“大家好,我是%s,今年%d岁,我的成绩%d分\n”,s.name,s.age,s.score)

}

func (s *student13)sing(name string) {

fmt.Println(“我为大家唱首歌”,name)

}

func main() {

//接口类型变量定义

var h Humaner2 //子集

var p Personer //超集

var stu student13 = student13{“小吴”,18,59}

p = &stu

//将一个接口赋值给另一个接口

//超集中包含所有子集的方法

h = p //ok

h.sayhi()

//子集不包含超集

//不能将子集赋值给超集

//p = h //err

//p.sayhi()

//p.sing(“大碗面”)

}

8.4.6 空接口

空接口(interface{})不包含任何的方法,正因为如此,所有的类型都实现了空接口,因此空接口可以存储任意类型的数值。例如:

var i interface{}

//接口类型可以接收任意类型的数据

//fmt.Println(i)

fmt.Printf(“%T\n”,i)

i = 10

fmt.Println(i)

fmt.Printf(“%T\n”,i)

当函数可以接受任意的对象实例时,我们会将其声明为interface{},最典型的例子是标准库fmt中PrintXXX系列的函数,例如:

func Printf(fmt string, args …interface{})

func Println(args …interface{})

如果自己定义函数,可以如下:

func Test(arg …interface{}) {

}

Test()函数可以接收任意个数,任意类型的参数。

8.5 类型查询


我们知道interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:

  • comma-ok断言

  • switch测试

8.5.1 comma-ok断言

Go语言里面有一个语法,可以直接判断是否是该类型的变量:value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,elementinterface变量,T是断言的类型。

如果element里面确实存储了T类型的数值,那么o``返回true,否则返回false`。

var i []interface{}

i = append(i, 10, 3.14, “aaa”, demo15)

for _, v := range i {

if data, ok := v.(int); ok {

fmt.Println(“整型数据:”, data)

} else if data, ok := v.(float64); ok {

fmt.Println(“浮点型数据:”, data)

} else if data, ok := v.(string); ok {

fmt.Println(“字符串数据:”, data)

} else if data, ok := v.(func()); ok {

//函数调用

data()

}

}

如果这个i中有v.(int)也就是int类型的数值就返回打印出来。

8.5.2 switch测试

var i []interface{}

i = append(i, 10, 3.14, “aaa”, demo15)

for _,data := range i{

switch value:=data.(type) {

case int:

fmt.Println(“整型”,value)

case float64:

fmt.Println(“浮点型”,value)

case string:

fmt.Println(“字符串”,value)

case func():

fmt.Println(“函数”,value)

}

}

这个也是一样的道理只不过是用了另外一种方法,data也就是里面的值,如果里面的值类型是int的话就打印出来这个值。

九、异常处理

=================================================================

9.1 error接口


Go语言引入了一个关于错误处理的标准模式,即error接口,它是Go语言内建的接口类型,该接口的定义如下:

type error interface {

Error() string

}

Go语言的标准库代码包errors为用户提供如下方法:

import92.png

通过以上代码,可以发现error接口的使用是非常简单的(error是一个接口,该接口只声明了一个方法Error(),返回值是string类型,用以描述错误)。下面看一下基本使用:

  1. 首先导包:

import “errors”

  1. 然后调用其对应的方法:

import93.png

当然fmt包中也封装了一个专门输出错误信息的方法,如下所示:

import94..png

了解完基本的语法以后,接下来使用error接口解决Test()函数被0整除的问题。如下所示:

impor95.png

Test()函数中,判断变量b的取值,如果有误,返回错误信息。并且在main()中接收返回的错误信息,并打印出来。

这种用法是非常常见的,例如,后面讲解到文件操作时,涉及到文件的打开,如下:

import96.png

在打开文件时,如果文件不存在,或者文件在磁盘上存储的路径写错了,都会出现异常,这时可以使用error记录相应的错误信息。

9.2 panic函数


error返回的是一般性的错误,但是panic函数返回的是让程序崩溃的错误。

也就是当遇到不可恢复的错误状态的时候,如数组访问越界、空指针引用等,这些运行时错误会引起panic异常,在一般情况下,我们不应通过调用panic函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式。当某些不应该发生的场景发生时,我们就应该调用panic

一般而言,当panic异常发生时,程序会中断运行。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。

当然,如果直接调用内置的panic函数也会引发panic异常panic函数接受任何值作为参数。

下面给大家演示一下,直接调用panic函数,是否会导致程序的崩溃。

import97.png

错误信息如下:

import98.png

所以,我们在实际的开发过程中并不会直接调用panic()函数,但是当我们编程的程序遇到致命错误时,系统会自动调用该函数来终止整个程序的运行,也就是系统内置了panic函数

下面给大家演示一个数组下标越界的问题:

import99.png

错误信息如下:

import100.png

通过观察错误信息,发现确实是panic异常,导致了整个程序崩溃。

9.3 延迟调用defer


9.3.1 defer基本使用

函数定义完成后,只有调用函数才能够执行,并且一经调用立即执行。例如:

fmt.Println(“hello”)

fmt.Println(“老王”)

先输出“hello”,然后再输出“老王”。但是关键字defer⽤于延迟一个函数(或者当前所创建的匿名函数)的执行。注意,defer语句只能出现在函数的内部。

基本用法如下:

defer fmt.Println(“hello”)

fmt.Println(“老王”)

以上两行代码,输出的结果为,先输出“老王”,然后输出“hello”。

defer的应用场景:文件操作,先打开文件,执行读写操作,最后关闭文件。为了保证文件的关闭能够正确执行,可以使用defer

9.3.2 defer执行顺序

先看如下程序执行结果是:

defer fmt.Println(“hello”)

defer fmt.Println(“老王”)

defer fmt.Println(“你好”)

执行的结果是:

你好

老王

hello

总结:如果一个函数中有多个defer语句,它们会以后进先出的顺序执行。

如下程序执行的结果:

func test03(x int) {

v := 100 / x

fmt.Println(v)

}

defer fmt.Println(“hello”)

defer fmt.Println(“老王”)

defer test03(0)

defer fmt.Println(“你好”)

执行结果:

你好

老王

hello

panic: runtime error: integer divide by zero

即使函数或某个延迟调用发生错误,这些调用依旧会被执⾏。

9.3.3 defer与匿名函数结合使用

我们先看以下程序的执行结果:

a := 10

b := 20

defer func() {

fmt.Println(“匿名函数a”, a)

fmt.Println(“匿名函数b”, b)

}()

a = 100

b = 200

fmt.Println(“main函数a”, a)

fmt.Println(“main函数b”, b)

执行的结果如下:

main函数a 100

main函数b 200

匿名函数a 100

匿名函数b 200

前面讲解过,defer会延迟函数的执行,虽然立即调用了匿名函数,但是该匿名函数不会执行,等整个main()函数结束之前在去调用执行匿名函数,所以输出结果如上所示。

现在将程序做如下修改:

a := 10

b := 20

defer func(a,b int) { //添加参数

fmt.Println(“匿名函数a”, a)

fmt.Println(“匿名函数b”, b)

}(a,b) //传参

a = 100

b = 200

fmt.Println(“main函数a”, a)

fmt.Println(“main函数b”, b)

该程序的执行结果如下:

main函数a 100

main函数b 200

匿名函数a 10

匿名函数b 20

从执行结果上分析,由于匿名函数前面加上了defer所以,匿名函数没有立即执行。但是问题是,程序从上开始执行当执行到匿名函数时,虽然没有立即调用执行匿名函数,但是已经完成了参数的传递。

9.4 recover函数


运行时panic异常一旦被引发就会导致程序崩溃。这当然不是我们愿意看到的,因为谁也不能保证程序不会发生任何运行时错误。

Go语言为我们提供了专用于“拦截”运行时panic的内建函数——recover。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。

注意:recover只有在defer调用的函数中有效。

示例如下:

package main

import “fmt”

func testA() {

fmt.Println(“testA”)

}

func testB(x int) {

//设置recover()

//在defer调用的函数中使用recover()

defer func() {

//防止程序崩溃

recover()

}() //匿名函数

var a [3]int

a[x] = 999

}

func testC() {

fmt.Println(“testC”)

}

func main() {

testA()

testB(3) //发生异常 中断程序

testC()

}

以上程序的运行结果如下:

testA

testC

通过以上程序,我们发现虽然TestB()函数会导致整个应用程序崩溃,但是由于在改函数中调用了recover()函数,所以整个函数并没有崩溃。虽然程序没有崩溃,但是我们也没有看到任何的提示信息,那么怎样才能够看到相应的提示信息呢?

可以直接打印recover()函数的返回结果,如下所示:

func testB(x int) {

//设置recover()

//在defer调用的函数中使用recover()

defer func() {

//防止程序崩溃

//recover()

fmt.Println(recover()) //直接打印

}() //匿名函数

var a [3]int

a[x] = 999

}

输出结果如下:

testA

runtime error: index out of range

testC

从输出结果发现,确实打印出了相应的错误信息。

但是,如果程序没有出错,也就是数组下标没有越界,会出现什么情况呢?

func testA() {

fmt.Println(“testA”)

}

func testB(x int) {

//设置recover()

//在defer调用的函数中使用recover()

defer func() {

//防止程序崩溃

//recover()

fmt.Println(recover())

}() //匿名函数

var a [3]int

a[x] = 999

}

func testC() {

fmt.Println(“testC”)

}

func main() {

testA()

testB(0) //发生异常 中断程序

testC()

}

输入的结果如下:

testA

testC

这时输出的是空,但是我们希望程序没有错误的时候,不输出任何内容。

所以,程序修改如下:

func testA() {

fmt.Println(“testA”)

}

func testB(x int) {

//设置recover()

//在defer调用的函数中使用recover()

defer func() {

//防止程序崩溃

//recover()

//fmt.Println(recover())

if err := recover();err != nil {

fmt.Println(err)

}

}() //匿名函数

var a [3]int

a[x] = 999

}

func testC() {

fmt.Println(“testC”)

}

func main() {

testA()

testB(0) //发生异常 中断程序

testC()

}

通过以上代码,发现其实就是加了一层判断。这样就不会使得程序崩溃。

十、文件操作

=================================================================

10.1 字符串处理


10.1.1 字符串处理函数

我们从文件中将数据读取出来以后,很多情况下并不是直接将数据打印出来,而是要做相应的处理。例如:去掉空格等一些特殊的符号,对一些内容进行替换等。

这里就涉及到对一些字符串的处理。在对字符串进行处理时,需要借助于包“strings”

下面讲解一下常用的字符串处理函数:

  1. Contains

func Contains(s, substr string) bool

功能:字符串s中是否包含substr,返回bool值。演示如下:

//查找一个字符串在另一个字符串中是否出现

str1 := “hello world”

str2 := “g”

//Contains(被查找的字符串,查找的字符串) 返回值 bool

//一般用于模糊查找

b := strings.Contains(str1,str2)

//fmt.Println(b)

if b {

fmt.Println(“找到了”)

}else {

fmt.Println(“没有找到”)

}

在使用Contains关键字的时候,判断b的结果,如果在str1中有str2的字那么就返回true,在判断的时候不写true默认就是等于true

  1. Join

func Join(a []string, sep string) string

功能:字符串链接,把slice通过sep链接起来

演示如下:

//字符串切片

slice := []string{“123”,“456”,“789”}

//fmt.Println(slice)

//Join

//字符串的连接

str := strings.Join(slice,“”)

fmt.Println(str)

//fmt.Printf(“%T\n”,str)

结果如下:

123456789

通过join关键字把,slice里面的值通过strings.Join(slice,"")也就是去除""给从新赋值给了str最后打印出来的值就变成了123456789

  1. Index

func Index(s, substr string) int

功能:在字符串s中查找sep所在的位置,返回位置值,找不到返回-1

str1 := “hello world”

str2 := “e”

//查找一个字符串在另一个字符串中第一次出现的位置 返回值 int 下标 -1 找不到

i := strings.Index(str1,str2)

fmt.Println(i)

结果为1。

i := strings.Index(str1,str2)通过index关键字,在str1中查找str2的值,然后赋值给ie这个值在hello world中能找到所以就会返回它的下标值,下标值是从0开始的,h0e就是1,所以结果为1。如果查找的是一个g的话找不到就会返回一个-1。

  1. Repeat

func Repeat(s string, count int) string

功能:重复s字符串count次,最后返回重复的字符串。

演示如下:

str := “性感网友,在线取名。”

//将一个字符串重复n次

str1 := strings.Repeat(str,100)

fmt.Println(str1)

str1 := strings.Repeat(str,100)通过repeat关键字重复了str100遍,就和循环遍历str100次是一样的。

  1. Replace

func Replace(s, old, new string, n int) string

功能:在s字符串中,把old字符串替换为new字符串,n表示替换的次数,小于0表示全部替换

str := “性感网友在线取名性感性感性感性感性感”

//字符串替换 屏蔽敏感词汇

//如果替换次数小于0 表示全部替换

str1 := strings.Replace(str,“性感”,“**”,-1)

fmt.Println(str1)

结果如下:

网友在线取名********

str1 := strings.Replace(str,"性感","**",-1)通过关键字replacestr中的性感替换为了******然后给了个-1也就是全部替换,当然你给其他的负数也是一样的,只要是小于0就全部替换,如果说是1的话就是替换一次,输出结果就会是:**网友在线取名性感性感性感性感性感

  1. Split

func Split(s, sep string) []string

功能:把s字符串按照sep分割,返回slice。

//将一个字符串按照标志位进行切割变成切片

str1 := “123456789@qq.com”

slice := strings.Split(str1,“@”)

fmt.Println(slice[0])

结果如下:

123456789

slice := strings.Split(str1,"@")通过split关键字对str1进行了分割,把@后面的给丢弃了,留下了@前面的。

  1. Trim

func Trim(s string, cutset string) string

功能:在s字符串的头部和尾部去除cutset指定的字符串。

str := “a=u=ok==”

//去掉字符串头尾的内容

str1:= strings.Trim(str,“=”)

fmt.Println(str1)

结果如下:

a===u=ok

str1:= strings.Trim(str,"=")通过Trim关键字对str中的=号做了去除头尾的处理,只要是str前面有=,或者后面有=都会去除。

  1. Fields

func Fields(s string) []string

功能:去除s字符串的空格符,并且按照空格分割返回slice

str := " are you ok "

//去除字符串中空格 转成切片 一般用于统计单词个数

slice := strings.Fields(str)

fmt.Println(slice)

slice := strings.Fields(str)通过Fields关键字对str中的值进行了空格去除。

二、字符串转换

通过以上的讲解,发现字符串的处理是非常重要的,GO语言也提供了字符串与其它类型之间相互转换的函数。相应的字符串转换函数都在“strconv”包。

  1. Format系列函数:

Format系列函数把其他类型的转换为字符串。

//将其他类型转成字符串 Format

b := false

str := strconv.FormatBool(true)

fmt.Println(str)

fmt.Printf(“%T\n”,str)

str := strconv.FormatInt(120,10) //计算机中进制 可以表示2-36 2 8 10 16

fmt.Println(str)

// 'f’打印方式 以小数方式 4 指小数位数 64 以float64处理

str:= strconv.FormatFloat(3.14159,‘f’,4,64)

fmt.Println(str)

str := strconv.Itoa(123)

fmt.Println(str)

以上代码只要是通过Format关键字就能全部转换为字符串类型输出。

  1. Parse

Parse系列函数把字符串转换为其他类型:

//字符串转成其他类型 Parse

b,err := strconv.ParseBool(“true”)

if err!=nil {

fmt.Println(“类型转换出错”)

}else {

fmt.Println(b)

fmt.Printf(“%T\n”,b)

}

v,err := strconv.ParseInt(“abc”,16,64)

fmt.Println(v,err)

v,_ := strconv.ParseFloat(“3.14159”,64)

fmt.Println(v)

v,_:=strconv.Atoi(“123”)

fmt.Println(v)

  1. Append

Append系列函数将整数等转换为字符串后,添加到现有的字节数组中。

slice := make([]byte,0,1024)

//将其他类型转成字符串 添加到字符切片里

slice = strconv.AppendBool(slice,false)

slice = strconv.AppendInt(slice,123,2)

slice = strconv.AppendFloat(slice,3.14159,‘f’,4,64)

slice = strconv.AppendQuote(slice,“hello”)

fmt.Println(string(slice))

对应的结果是:

false11110113.1416"hello"

通过Append关键字把其他类型的值转换成字符串后在拼接到一起,赋值给slice,当然赋值给谁你可以自己起名字,所以最后的打印结果是上面这样的。

10.2 创建文件


将数据存储到文件之前,先要创建文件。GO语言中提供了一个Create()函数专门创建文件。

该函数在创建文件时,首先会判断要创建的文件是否存在,如果不存在,则创建,如果存在,会先将文件中已有的数据清空。

同时,当文件创建成功后,该文件会默认的打开,所以不用在执行打开操作,可以直接向该文件中写入数据。

创建文件的步骤:

  1. 导入“os”包,创建文件,读写文件的函数都在该包。

  2. 指定创建的文件存放路径以及文件名。

  3. 执行Create()函数,进行文件创建。

  4. 关闭文件。

具体代码如下:

package main

import (

“fmt”

“os”

)

func main() {

//os.Create(文件名) 文件名 可以写绝对路径和相对路径

//返回值 文件指针 错误信息

fp,err := os.Create(“./a.txt”)

if err!=nil{

//文件创建失败

/*

1.路径不存在

2.文件权限

3.程序打开文件上限

*/

fmt.Println(“文件创建失败”)

return

}

//读写文件

defer fp.Close()

//关闭文件

//如果打开文件不关闭 造成内存的浪费 程序打开文件的上限

//fp.Close()

}

执行以上代码后,可以在程序文件存放的目录中,看到有一个a.txt的文件。

注意:在创建的文件时,注意需要判断是否出现异常,同时要注意defer的应用。

10.3 写入数据


文件打开以后,可以向文件中写数据,可以使用WriteString( )方法。

//\反斜杠 转义字符

//在写路径时可以使用/正斜杠代替\反斜杠

fp,err := os.Create(“D:/a.txt”)

if err!=nil{

//文件创建失败

/*

1.路径不存在

2.文件权限

3.程序打开文件上限

*/

fmt.Println(“文件创建失败”)

return

}

//写文件

//\n不会换行 原因 在windows文本文件中换行\r\n 回车 在linux中换行\n

fp.WriteString(“hello world\r\n”)

fp.WriteString(“性感荷官在线发牌”)

defer fp.Close()

//关闭文件

//如果打开文件不关闭 造成内存的浪费 程序打开文件的上限

//fp.Close()

WriteString()方法默认返回两个参数:

count,err1 := fp.WriteString(“性感老王在线授课”)

if err1!=nil {

fmt.Println(“写入文件失败”)

return

}else {

fmt.Println(count)

}

第一个参数,指的是写入文件的数据长度,第二个参数记录的是错误信息。

WriteString()方法默认写到文件中的数据是不换行的。如果想换行,可以采用如下的方式:

//\n不会换行 原因 在windows文本文件中换行\r\n 回车 在linux中换行\n

fp.WriteString(“hello world\r\n”)

fp.WriteString(“性感荷官在线发牌”)

除了使用WriteString()函数向文件中写入数据以外,还可以使用Write()函数,如下所示:

fp,err := os.Create(“D:/a.txt”)

if err!=nil{

//文件创建失败

/*

1.路径不存在

2.文件权限

3.程序打开文件上限

*/

fmt.Println(“文件创建失败”)

return

}

//写操作

//slice := []byte{‘h’,‘e’,‘l’,‘l’,‘o’}

//count,err1 := fp.Write(slice)

count,err1 := fp.Write([]byte(“性感老王在线授课”))

if err1!=nil {

fmt.Println(“写入文件失败”)

return

}else {

fmt.Println(count)

}

defer fp.Close()

在这里要注意的是,使用Write()函数写数据时,参数为字节切片,所以需要将字符串转换成字节切片。该方法返回的也是写入文件数据的长度。

第三种写入的方式使用WriteAt()函数,在指定的位置写入数据:

fp,err := os.Create(“D:/a.txt”)

if err!=nil{

//文件创建失败

/*

1.路径不存在

2.文件权限

3.程序打开文件上限

*/

fmt.Println(“文件创建失败”)

return

}

//写操作

//获取光标流位置’

//获取文件起始到结尾有多少个字符

//count,_:=fp.Seek(0,os.SEEK_END)

count,_:=fp.Seek(0,io.SeekEnd)

fmt.Println(count)

//指定位置写入

fp.WriteAt([]byte(“hello world”),count)

fp.WriteAt([]byte(“hahaha”),0)

fp.WriteAt([]byte(“秀儿”),19)

defer fp.Close()

以上程序中Seek()函数返回值存储到变量n中,值为文件末尾的位置。WriteAt()也返回的是写入的数据长度。

以上就是我们常用的关于向文件中写入数据的方式,但是有同学可能有疑问,每次向文件中写入数据之前,都是先执行了,Create()这个函数,而这个函数的作用前面我们也已经说过。有两个作用:

  • 第一:创建新文件。

  • 第二:如果所创建的文件已经存在,会删除掉文件中存储的数据。

那么,现在怎样向已有的文件中追加数据呢?如果要解决这个问题,那么大家一定要注意的就是,对已经存在的文件不能再执行Create(),而是要执行OpenFile()。如下所示:

//os.Open 只读方式打开

//fp,err := os.Open(“D:/a.txt”)

//os.OpenFile(文件名,打开方式,打开权限)

fp,err := os.OpenFile(“D:/a.txt”,os.O_RDWR,6)

if err!=nil {

fmt.Println(“打开文件失败”)

}

fp.WriteString(“hello”)

fp.WriteAt([]byte(“hello”),25)

defer fp.Close()

OpenFile()这个函数有三个参数,第一个参数表示打开文件的路径,第二个参数表示模式,常见的模式有

O_RDONLY(只读模式)O_WRONLY(只写模式)O_RDWR(可读可写模式)O_APPEND(追加模式)

第三个参数,表示权限,取值范围(0-7)

表示如下:

  • 0:没有任何权限

  • 1:执行权限(如果是可执行文件,是可以运行的)

  • 2:写权限

  • 3: 写权限与执行权限

  • 4:读权限

  • 5: 读权限与执行权限

  • 6: 读权限与写权限

  • 7: 读权限,写权限,执行权限

10.4 读取文件


10.4.1 Read 读取文件

如果文件已经存在,并且也已经有数据了,那么可以直接读取该文件中的内容。

读取文件的基本流程如下:

  1. 打开要读取的文件

  2. 对文件进行读取

  3. 关闭文件

在向文件中写数据的时候,使用的是Write,那么读取文件中的数据,使用的是Read

关于Read()函数的使用如下:

package main

import (

“fmt”

“io”

“os”

)

func main() {

//打开文件

fp, err := os.Open(“D:/a.txt”)

if err != nil {

fmt.Println(“err=”, err)

return

}

buf := make([]byte, 1024*2) //2k大小

//n代表从文件读取内容的长度

n, err1 := fp.Read(buf)

if err1 != nil && err1 != io.EOF {

fmt.Println(“err1=”, err1)

return

}

fmt.Println(“buf=”, string(buf[:n]))

//关闭文件

defer fp.Close()

}

Open()是打开文件,与OpenFile()的区别是,Open()只有读的权限。

在使用Read()函数读取文件中的内容时,需要一个切片类型,而定义切片时类型为字符数组,将文件中的内容保存在切片中,同时除了对其判断是否出错时以外,还要判断是否到文件末尾(这里需要导入io包)。

Read()函数返回的是从文件中读取的数据的长度。最后,输出切片中存储的文件数据,注意,读取的是从最开始到整个数据长度,因为有可能存储到切片中的数据达不到切片的总长度(也是切片时2k,但是从文件中读取的数据有可能只有1k)。

10.4.2 按行读取

上面我们是将文件的内容全部读取出来,然后存放在切片中,我们也可以每次只读取一行数据。

这需要用到bufio包中的ReadBytes函数。具体如下:

  1. 打开文件

fp, err := os.Open(“D:/a.txt”)

if err != nil {

fmt.Println(“打开文件失败”, err)

return

}

  1. 创建缓冲区

在使用ReadBytes()函数读取数据时,需要用到缓冲区,所谓缓冲区就是存储数据的区域,也就是先将从文件中读取的数据存储在该区域内,然后在将区域中的数据取出来,写到磁盘上。提供缓冲区的原因是:

为了缓和CPU与磁盘设备之间速度不匹配矛盾。文件缓冲区是用以暂时存放读写期间的文件数据而在内存区预留的一定空间。

//创建文件缓冲区

r := bufio.NewReader(fp)

  1. 循环读取文件中的内容,直到文件末尾位置。

for {

//遇到’\n’结束读取,但是’\n’也读取进入

buf,err := r.ReadBytes(‘\n’)

fmt.Println("buf = ",string(buf))

if err != nil {

if err == io.EOF {

break

}

fmt.Println(“err=”,err)

}

}

在使用ReadBytes()函数时,传递的参数是\n,表示遇到\n就结束,所以使用了死循环(每循环一次,读取一行数据),只有到文件末尾了,才退出整个循环。最后,将读取的数据打印出来,注意ReadBytes()返回的是字节切片,所以在打印时要转换成字符串。

  1. 最后关闭文件

//关闭文件

defer fp.Close()

现在我们已经完成了文件的创建,读取,以及将数据保存到文件的操作,在对文件操作时,我们需要指定文件的路径。

关于路径,有两种情况:

  • 第一:相对路径,所谓相对路径指的是文件相对于应用程序的路径。例如:上面我们一只使用的a.txt,这个文件,该文件存放的位置与可执行文件存储的路径是一样的。

  • 第二:绝对路径:指的是通过给定的这个路径直接能在我的电脑中找到这个文件。例如:D:\Info.txt,

  • 建议我们以后在开发中使用相对路径

10.4 文件操作案例


文件拷贝,将已有的文件复制一份,同时重新命名。

基本的思路:

  1. 让用户输入要拷贝的文件的名称(源文件)以及目的文件的名称

  2. 创建目的文件

  3. 打开源文件,并且读取该文件中的内容

  4. 将从源文件中读取的内容写到目的文件中。

var srcFileName string

var dstFileName string

fmt.Printf(“请输入源文件名称:”)

fmt.Scan(&srcFileName)

fmt.Println(“请输入目的文件名称:”)

fmt.Scan(&dstFileName)

if srcFileName == dstFileName {

fmt.Println(“源文件和目的文件名字不能相同”)

return

}

//只读方式打开源文件

sF,err1 := os.Open(srcFileName)

if err1 != nil {

fmt.Println(“err1=”,err1)

return

}

//新建目的文件

dF,err2 := os.Create(dstFileName)

if err2 != nil{

fmt.Println(“err2=”,err2)

return

}

//操作完毕,需要关闭文件

defer sF.Close()

defer dF.Close()

//核心处理,从源文件读取内容,往目的文件写,读多少写多少

buf := make([]byte,4*1024)//4k大小临时缓冲区

for{

n,err := sF.Read(buf)//从源文件读取内容,每次读取一部分

if err != nil{

fmt.Println(“err=”,err)

if err == io.EOF{//文件读取完毕

break

}

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

10.3 写入数据


文件打开以后,可以向文件中写数据,可以使用WriteString( )方法。

//\反斜杠 转义字符

//在写路径时可以使用/正斜杠代替\反斜杠

fp,err := os.Create(“D:/a.txt”)

if err!=nil{

//文件创建失败

/*

1.路径不存在

2.文件权限

3.程序打开文件上限

*/

fmt.Println(“文件创建失败”)

return

}

//写文件

//\n不会换行 原因 在windows文本文件中换行\r\n 回车 在linux中换行\n

fp.WriteString(“hello world\r\n”)

fp.WriteString(“性感荷官在线发牌”)

defer fp.Close()

//关闭文件

//如果打开文件不关闭 造成内存的浪费 程序打开文件的上限

//fp.Close()

WriteString()方法默认返回两个参数:

count,err1 := fp.WriteString(“性感老王在线授课”)

if err1!=nil {

fmt.Println(“写入文件失败”)

return

}else {

fmt.Println(count)

}

第一个参数,指的是写入文件的数据长度,第二个参数记录的是错误信息。

WriteString()方法默认写到文件中的数据是不换行的。如果想换行,可以采用如下的方式:

//\n不会换行 原因 在windows文本文件中换行\r\n 回车 在linux中换行\n

fp.WriteString(“hello world\r\n”)

fp.WriteString(“性感荷官在线发牌”)

除了使用WriteString()函数向文件中写入数据以外,还可以使用Write()函数,如下所示:

fp,err := os.Create(“D:/a.txt”)

if err!=nil{

//文件创建失败

/*

1.路径不存在

2.文件权限

3.程序打开文件上限

*/

fmt.Println(“文件创建失败”)

return

}

//写操作

//slice := []byte{‘h’,‘e’,‘l’,‘l’,‘o’}

//count,err1 := fp.Write(slice)

count,err1 := fp.Write([]byte(“性感老王在线授课”))

if err1!=nil {

fmt.Println(“写入文件失败”)

return

}else {

fmt.Println(count)

}

defer fp.Close()

在这里要注意的是,使用Write()函数写数据时,参数为字节切片,所以需要将字符串转换成字节切片。该方法返回的也是写入文件数据的长度。

第三种写入的方式使用WriteAt()函数,在指定的位置写入数据:

fp,err := os.Create(“D:/a.txt”)

if err!=nil{

//文件创建失败

/*

1.路径不存在

2.文件权限

3.程序打开文件上限

*/

fmt.Println(“文件创建失败”)

return

}

//写操作

//获取光标流位置’

//获取文件起始到结尾有多少个字符

//count,_:=fp.Seek(0,os.SEEK_END)

count,_:=fp.Seek(0,io.SeekEnd)

fmt.Println(count)

//指定位置写入

fp.WriteAt([]byte(“hello world”),count)

fp.WriteAt([]byte(“hahaha”),0)

fp.WriteAt([]byte(“秀儿”),19)

defer fp.Close()

以上程序中Seek()函数返回值存储到变量n中,值为文件末尾的位置。WriteAt()也返回的是写入的数据长度。

以上就是我们常用的关于向文件中写入数据的方式,但是有同学可能有疑问,每次向文件中写入数据之前,都是先执行了,Create()这个函数,而这个函数的作用前面我们也已经说过。有两个作用:

  • 第一:创建新文件。

  • 第二:如果所创建的文件已经存在,会删除掉文件中存储的数据。

那么,现在怎样向已有的文件中追加数据呢?如果要解决这个问题,那么大家一定要注意的就是,对已经存在的文件不能再执行Create(),而是要执行OpenFile()。如下所示:

//os.Open 只读方式打开

//fp,err := os.Open(“D:/a.txt”)

//os.OpenFile(文件名,打开方式,打开权限)

fp,err := os.OpenFile(“D:/a.txt”,os.O_RDWR,6)

if err!=nil {

fmt.Println(“打开文件失败”)

}

fp.WriteString(“hello”)

fp.WriteAt([]byte(“hello”),25)

defer fp.Close()

OpenFile()这个函数有三个参数,第一个参数表示打开文件的路径,第二个参数表示模式,常见的模式有

O_RDONLY(只读模式)O_WRONLY(只写模式)O_RDWR(可读可写模式)O_APPEND(追加模式)

第三个参数,表示权限,取值范围(0-7)

表示如下:

  • 0:没有任何权限

  • 1:执行权限(如果是可执行文件,是可以运行的)

  • 2:写权限

  • 3: 写权限与执行权限

  • 4:读权限

  • 5: 读权限与执行权限

  • 6: 读权限与写权限

  • 7: 读权限,写权限,执行权限

10.4 读取文件


10.4.1 Read 读取文件

如果文件已经存在,并且也已经有数据了,那么可以直接读取该文件中的内容。

读取文件的基本流程如下:

  1. 打开要读取的文件

  2. 对文件进行读取

  3. 关闭文件

在向文件中写数据的时候,使用的是Write,那么读取文件中的数据,使用的是Read

关于Read()函数的使用如下:

package main

import (

“fmt”

“io”

“os”

)

func main() {

//打开文件

fp, err := os.Open(“D:/a.txt”)

if err != nil {

fmt.Println(“err=”, err)

return

}

buf := make([]byte, 1024*2) //2k大小

//n代表从文件读取内容的长度

n, err1 := fp.Read(buf)

if err1 != nil && err1 != io.EOF {

fmt.Println(“err1=”, err1)

return

}

fmt.Println(“buf=”, string(buf[:n]))

//关闭文件

defer fp.Close()

}

Open()是打开文件,与OpenFile()的区别是,Open()只有读的权限。

在使用Read()函数读取文件中的内容时,需要一个切片类型,而定义切片时类型为字符数组,将文件中的内容保存在切片中,同时除了对其判断是否出错时以外,还要判断是否到文件末尾(这里需要导入io包)。

Read()函数返回的是从文件中读取的数据的长度。最后,输出切片中存储的文件数据,注意,读取的是从最开始到整个数据长度,因为有可能存储到切片中的数据达不到切片的总长度(也是切片时2k,但是从文件中读取的数据有可能只有1k)。

10.4.2 按行读取

上面我们是将文件的内容全部读取出来,然后存放在切片中,我们也可以每次只读取一行数据。

这需要用到bufio包中的ReadBytes函数。具体如下:

  1. 打开文件

fp, err := os.Open(“D:/a.txt”)

if err != nil {

fmt.Println(“打开文件失败”, err)

return

}

  1. 创建缓冲区

在使用ReadBytes()函数读取数据时,需要用到缓冲区,所谓缓冲区就是存储数据的区域,也就是先将从文件中读取的数据存储在该区域内,然后在将区域中的数据取出来,写到磁盘上。提供缓冲区的原因是:

为了缓和CPU与磁盘设备之间速度不匹配矛盾。文件缓冲区是用以暂时存放读写期间的文件数据而在内存区预留的一定空间。

//创建文件缓冲区

r := bufio.NewReader(fp)

  1. 循环读取文件中的内容,直到文件末尾位置。

for {

//遇到’\n’结束读取,但是’\n’也读取进入

buf,err := r.ReadBytes(‘\n’)

fmt.Println("buf = ",string(buf))

if err != nil {

if err == io.EOF {

break

}

fmt.Println(“err=”,err)

}

}

在使用ReadBytes()函数时,传递的参数是\n,表示遇到\n就结束,所以使用了死循环(每循环一次,读取一行数据),只有到文件末尾了,才退出整个循环。最后,将读取的数据打印出来,注意ReadBytes()返回的是字节切片,所以在打印时要转换成字符串。

  1. 最后关闭文件

//关闭文件

defer fp.Close()

现在我们已经完成了文件的创建,读取,以及将数据保存到文件的操作,在对文件操作时,我们需要指定文件的路径。

关于路径,有两种情况:

  • 第一:相对路径,所谓相对路径指的是文件相对于应用程序的路径。例如:上面我们一只使用的a.txt,这个文件,该文件存放的位置与可执行文件存储的路径是一样的。

  • 第二:绝对路径:指的是通过给定的这个路径直接能在我的电脑中找到这个文件。例如:D:\Info.txt,

  • 建议我们以后在开发中使用相对路径

10.4 文件操作案例


文件拷贝,将已有的文件复制一份,同时重新命名。

基本的思路:

  1. 让用户输入要拷贝的文件的名称(源文件)以及目的文件的名称

  2. 创建目的文件

  3. 打开源文件,并且读取该文件中的内容

  4. 将从源文件中读取的内容写到目的文件中。

var srcFileName string

var dstFileName string

fmt.Printf(“请输入源文件名称:”)

fmt.Scan(&srcFileName)

fmt.Println(“请输入目的文件名称:”)

fmt.Scan(&dstFileName)

if srcFileName == dstFileName {

fmt.Println(“源文件和目的文件名字不能相同”)

return

}

//只读方式打开源文件

sF,err1 := os.Open(srcFileName)

if err1 != nil {

fmt.Println(“err1=”,err1)

return

}

//新建目的文件

dF,err2 := os.Create(dstFileName)

if err2 != nil{

fmt.Println(“err2=”,err2)

return

}

//操作完毕,需要关闭文件

defer sF.Close()

defer dF.Close()

//核心处理,从源文件读取内容,往目的文件写,读多少写多少

buf := make([]byte,4*1024)//4k大小临时缓冲区

for{

n,err := sF.Read(buf)//从源文件读取内容,每次读取一部分

if err != nil{

fmt.Println(“err=”,err)

if err == io.EOF{//文件读取完毕

break

}

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-bsItW6Cd-1713313968737)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 15
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值