Go基础,2024年最新Golang入门零基础

60 阅读22分钟

switch score {

case 90:

fmt.Println("A")

//fallthrough

case 80:

fmt.Println("B")

//fallthrough

case 50,60,70:

fmt.Println("C")

//fallthrough

default:

fmt.Println("D")

}

//其实和if语句是差不多的道理,switch 后面就写你的条件比如上面的分数

//然后进入带着分数进入第一个case,如果分数等于90那我就打印A如果不等于

//我就进入下一条case。如果全部都没有匹配的那我们就进入default

可以使用任何类型或表达式作为条件语句:

//1

switch s1:=90 ; s1{//初始化语句;条件

case 90:

fmt.Println("A")

case 80:

fmt.Println("B")

default:

fmt.Println("C")

}

//这里是一样的只是在switch 后面定义了一个初始化变量,switch s1:=90 其实就是

//不用到上面定义var s1 int =90,switch s1和switch s1:=90 是一样的道理

//2

var s2 int=90

switch{ //这里没有写条件

case s2 >= 90: //这里写判断语句

fmt.Println("A")

cases2 >= 80:

fmt.Println("B")

default:

fmt.Println("C")

}

//这里的话就用上了条件了,,switch后面就不用写变量了,直接写花括号就行,

//我们在case 后面去判断,就和if 后面加条件一抹一样

//3

switch s3 := 90;{ //只有初始化语句,没有条件

case s3 >= 90: //这里写判断语句

fmt.Println("A")

case s3 >= 80:

fmt.Println("B")

default:

fmt.Println("C")

}

//这里就是结合了初始化变量和条件语句

4.1.3 区别

  1. if-else语句更适合于对区间(范围)的判断,而switch语句更适合于对离散值的判断。

  2. switch语句只支持常量值相等的分支判断,而if语句支持更为灵活,任意布尔表达式均可。

  3. switch语句通常比一系列嵌套if语句效率更高;逻辑更加清晰。

4.2 循环语句


4.2.1 for

我们为什么要使用循环语句呢?如果我要大家打印一个"今天我吃饭了"那是不是fmt.println("今天我吃饭了")就行了,但是我要大家打印100次呢,那要复制100次,循环语句就是重复执行某个事情,直到执行到了你相对应的那个条件就会结束。如下:

var i,sum int

for i = 1; i <= 100; i++ {

sum+=i

}

fmt.Println("sum=",sum)

4.2.2 range

关键字 range 会返回两个值,第一个返回值是元素的数组下标,第二个返回值是元素的值:

s := "abc"

for i := range s{//支持string/array/slice/map。

fmt.Printf("%c\n",s[i])

}

for _, c := range s{//忽略index

fmt.Printf("%c\n",c)

}

for i, c := range s{

fmt.Printf("%d,%c\n",i,c)

}

for i,c := range s这里面的i,c是自己定义的名字,你可以随便起,i是下标,c代表循环到的值。

4.2.3 跳出循环(break、continue、goto):

在循环里面有两个关键操作break和continue,break操作是跳出当前循环,continue是跳过本次循环。

for i := 0; i < 5; i++ {

if 2 == i {

//break操作是跳出当前循环

continue //continue是跳过本次循环

}

fmt.Println(i)

}

fmt.Println("aaaa")

goto END

fmt.Println("bbbb")

END:

fmt.Println("cccc")

break是跳出整个循环,剩下还没执行的循环也一并退出。

continue是跳出当前循环,会跳过当前还没有执行完的剩余代码。但是剩余的循环还是会执行。

goto是跳转

五、函数

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

5.1 函数基本使用


5.1.1 定义格式

函数构成代码执行的逻辑结构。在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。

Go 语言函数定义格式如下:

func FuncName(/参数列表/) (o1type1,o2type2/返回类型/) {

//函数体

return v1, v2//返回多个值

}

使用函数主要是为了方便我们以后一些大量代码的调用。

func FuncName就是定义一个函数给他起个名字,func FuncName (a int, b int)在这个里面可以传两个int值,func FuncName (a int, b int)(va int,vb int){}中的vavb是函数返回值。

5.1.2 无参无返回值

func Test() {//无参无返回值函数定义

fmt.Println("this is a test func")

}

func main() {

Test() //无参无返回值函数调用

}

这个是最简单的函数了,只要在main里面调用就行,Test()然后就会打印,this is a test func

5.1.3 有参无返回值

  1. 普通参数列表

func Test01(v1int,v2int) {//方式1

fmt.Printf("v1=%d,v2=%d\n",v1,v2)

}

func Test02(v1,v2int) {//方式2, v1, v2都是int类型

fmt.Printf("v1=%d,v2=%d\n",v1,v2)

}

func main() {

Test01(10,20) //函数调用

Test02(11,22) //函数调用

}

意思就是我Test01明确表明了我的参数是两个int类型,Test02里面的v1就没有明确表明是什么类型,其实都是一样的,然后在main里面调用展示一下。

  1. 不定参数列表

① 不定参数类型

不定参数是指函数传入的参数个数为不定数量。为了做到这点,首先需要将函数定义为接受不定参数类型:

//形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数

func Test(args...int) {

for_,n := range args{//遍历参数列表

fmt.Println(n)

}

}

func main() {

//函数调用,可传0到多个参数

Test()

Test(1)

Test(1,2,3)

}

Test(args...int)代表不确定有多少个int类型的参数要放到里面。

② 不定参数的传递

func MyFunc01(args...int) {

fmt.Println("MyFunc01")

for_,n := range args{//遍历参数列表

fmt.Println(n)

}

}

func MyFunc02(args...int) {

fmt.Println("MyFunc02")

for_,n:=range args{//遍历参数列表

fmt.Println(n)

}

}

func Test(args...int) {

MyFunc01(args...)//按原样传递,Test()的参数原封不动传递给MyFunc01

MyFunc02(args[1:]...)//Test()参数列表中,第1个参数及以后的参数传递给MyFunc02

}

func main() {

Test(1,2,3)//函数调用

}

Test里面调用MyFunc01MyFunc02,并且把所有参数都传给了MyFunc01,把除了第0个参数外的其他参数传给了MyFunc02

不定参数传参列表一定要放到最后

函数调用,固定参数必须传值,不定参数,根据需要选择是否传值

不同函数之间不定参数的传递

  1. 直接传

  2. 传递指定[i,j) i,j为下标

  3. 传递所有

func sum(args ...int) {

fmt.Println(args)

}

func test03(args ...int) {

// 1. 直接全

sum(args[0], args[1])

// 2. 指定传递[0,n)

sum(args[0:10]...)

// 3. 传递所有

sum(args[:]...)

sum(args...)

}

5.1.4 无参有返回值

有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

  1. 一个返回值

//方式1

func Test01() int {

return 250

}

// 这个int就是定义的返回值类型,也就是说你的返回值只能是int类型,不能回其他类型

// 官方建议:最好命名返回值func Test01()(a int){ a=250 return a}

// 因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差

//方式2,给返回值命名

func Test02() (value int) {

value=250

return value

}

//方式3,给返回值命名

func Test03() (value int) {

value = 250

return

}

func main(){

v1 := Test01()//函数调用

v2 := Test02()//函数调用

v3 := Test03()//函数调用

fmt.Printf("v1=%d,v2=%d,v3=%d\n", v1, v2, v3)

}

  1. 多个返回值

//方式1

func Test01() (int,string) {

return 88,"zhiliao"

}

//方式2,给返回值命名

func Test02() (a int,str string) {

a = 88

str = "zhiliao"

return a,str

}

func main(){

v1,v2 := Test01() // 函数调用

_,v3 := Test02() // 函数调用,第一个返回值丢弃

v4,_ := Test02() // 函数调用,第二个返回值丢弃

fmt.Printf("v1=%d,v2=%s,v3=%s,v4=%d\n",v1,v2,v3,v4)

}

5.1.5 有参有返回值

//求2个数的最小值和最大值

func MinAndMax(num1 int, num2 int) (min int, max int) {

if num1 > num2{//如果num1大于num2

min = num2

max = num1

} else {

max = num2

min = num1

}

return

}

func main() {

min,max := MinAndMax(33,22)

fmt.Printf("min=%d,max=%d\n", min, max) //min=22,max=33

}

用两个参数和两个返回值,在我们掉用方法的时候就会传两个参数3222然后我们进去判断一下如果3222大那么我们就把22赋值给返回值min,然后把32赋值给返回值max,然后return出去就好了。

5.2 递归函数


递归指函数可以直接或间接的调用自身。

递归函数通常有相同的结构:一个跳出条件和一个递归体。所谓跳出条件就是根据传入的参数判断是否需要停止递归,而递归体则是函数自身所做的一些处理。

// 通过循环实现1+2+3……+100

func Test01() int {

i := 1

sum := 0

for i = 1; i <= 100; i++ {

sum += i

}

return sum

}

// 通过递归实现1+2+3……+100

func Test02(num int) int {

if num == 1 {

return1

}

return num + Test02(num-1) // 函数调用本身

}

// 通过递归实现1+2+3……+100

func Test03(num int) int {

if num == 100 {

return 100

}

return num + Test03(num+1) // 函数调用本身

}

func main() {

fmt.Println(Test01()) // 5050

fmt.Println(Test02(100)) // 5050

fmt.Println(Test03(1)) // 5050

}

5.3 函数类型


Go语言中,函数也是一种数据类型,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型。

type FuncType func(int,int) int //声明一个函数类型,func后面没有函数名

// 函数中有一个参数类型为函数类型:fFuncType

func Calc(a, b int,f FuncType) (result int) {

result = f(a, b) //通过调用f()实现任务

return

}

func Add(a, b int) int {

return a + b

}

func Minus(a, b int) int {

return a - b

}

func main() {

//函数调用,第三个参数为函数名字,此函数的参数,返回值必须和FuncType类型一致

result := Calc(1, 1, Add)

fmt.Println(result) //2

var f FuncType = Minus

fmt.Println("result=",f(10, 2)) //result=8

}

type FuncType func(int, int)int这里就是声明一个函数类型,并且返回值为int类型。后面就可以把FuncType当做一个变量类型来使用了。

5.4 匿名函数与闭包


所谓闭包就是一个函数“捕获”了和它在同一作用域的其它常量和变量。这就意味着当闭包被调用的时候,不管在程序什么地方调用,闭包能够使用这些常量或者变量。它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只有闭包还在使用它,这些变量就还会存在。

在Go语言里,所有的匿名函数(Go语言规范中称之为函数字面量)都是闭包。匿名函数是指不需要定义函数名的一种函数实现方式,它并不是一个新概念,最早可以回溯到1958年的Lisp语言。

func main() {

i := 0

str := "mike"

// 方式1

f1 := func(){ //匿名函数,无参无返回值

//引用到函数外的变量

fmt.Printf("方式1:i=%d,str=%s\n",i,str)

}

f1() // 函数调用

// 方式1的另一种方式

type FuncType func() // 声明函数类型,无参无返回值

var f2 FuncType = f1

f2() // 函数调用

//方式2

var f3 FuncType = func(){

fmt.Printf("方式2:i=%d,str=%s\n",i,str)

}

f3() // 函数调用

//方式3

func(){ // 匿名函数,无参无返回值

fmt.Printf("方式3:i=%d,str=%s\n",i,str)

}() // 别忘了后面的(),()的作用是,此处直接调用此匿名函数

//方式4,匿名函数,有参有返回值

v := func(a,b int) (result int) {

result=a+b

return

}(1,1) // 别忘了后面的(1,1),(1,1)的作用是,此处直接调用此匿名函数,并传参

fmt.Println("v=",v)

}

5.4.1 闭包捕获外部变量特点

func main() {

i := 10

str := "mike"

func(){

i = 100

str = "go"

// 内部:i=100,str=go

fmt.Printf("内部:i=%d,str=%s\n",i,str)

}() // 别忘了后面的(),()的作用是,此处直接调用此匿名函数

// 外部:i=100,str=go

fmt.Printf("外部:i=%d,str=%s\n",i,str)

}

5.4.2 函数返回值为匿名函数

//squares返回一个匿名函数,func()int

//该匿名函数每次被调用时都会返回下一个数的平方。

func squares() func() int {

var x int

return func() int { // 匿名函数

x++ // 捕获外部变量

return x*x

}

}

func main() {

f:=squares()

fmt.Println(f()) //"1"

fmt.Println(f()) //"4"

fmt.Println(f()) //"9"

fmt.Println(f()) //"16"

}

函数squares返回另一个类型为func() int的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用时匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用squares时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。

通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。

5.5 函数作用域


5.5.1 局部变量

前面我们定义的函数中,都经常使用变量。那么我们看一下如下程序的输出结果:

func Test() {

a := 5

a += 1

}

func main() {

a := 9

Test()

fmt.Println(a)

}

最终的输出结果是9,为什么呢?在执行fmt.Println(a)语句之前,我们已经调用了函数Test(),并在该函数中我们已经重新给变量a赋值了。但是为什么结果没有发生变化呢?这就是变量的作用范围(作用域)的问题。在Test()函数中定义的变量a,它的作用范围只在改函数中有效,当Test()函数执行完成后,在该函数中定义的变量也就无效了。也就是说,当Test()函数执行完以后,定义在改函数中所有的变量,所占有的内存空间都会被回收。

所以,我们把定义在函数内部的变量称为局部变量。

局部变量的作用,为了临时保存数据需要在函数中定义变量来进行存储,这就是它的作用。

并且,通过上面的案例我们发现:不同的函数,可以定义相同的名字的局部变量,但是各用个的不会产生影响。例如:我们在main()函数中定义变量a,在Test()函数中也定义了变量a,但是两者之间互不影响,就是因为它们属于不同的函数,作用范围不一样,在内存中是两个存储区域。

5.5.2 全局变量

有局部变量,那么就有全局变量。所谓的全局变量:既能在一个函数中使用,也能在其他的函数中使用,这样的变量就是全局变量.也就是定义在函数外部的变量就是全局变量。全局变量在任何的地方都可以使用。案例如下:

var a int //变量定义在函数外面

func Test() {

a := 5

a += 1

}

func main() {

a := 9

Test()

fmt.Println(a)

}

注意:在上面的案例中,我们在函数外面定义了变量a,那么该变量就是全局变量,并且Test()函数和main()函数都可以使用该变量。该程序的执行流程是:先执行main()函数,给变量a赋值为9,紧接着调用Test()函数,在改函数中完成对变量a的修改。

由于main()函数与Test()函数所使用的变量a是同一个,所以当`Test( )函数执行完成后,变量的a已经变成了6.回到main( )函数执行后面的代码,也就是 fmt.Println(a),输出的值就是6.

可能有同学已经发现该程序和我们前面写的程序还有一点不同的地方是:第一个程序我们是a:=9,但是第二个程序执行修改成了a=9,现在修改一下第二个程序如下:

var a int //变量定义在函数外面

func Test() {

a := 5

a += 1

}

func main() {

a := 9 // ====》注意这里

Test()

fmt.Println(a)

}

该程序与上面的程序不同之处在于,该程序是a:=9,上面的程序是a=9

现在大家思考一下该程序的结果是多少?最终结果是9。原因是a:=9等价于:

var a int

a=9

也就是定义一个整型变量a,并且赋值为9

那么现在的问题是,我们定义了一个全局变量a,同时在main()中又定义了一个变量也叫a,但是该变量是一个局部变量。

当全局变量与局部变量名称一致时,局部变量的优先级要高于全局变量。所以在main()函数中执行fmt.Println(a)时输出的是局部变量a的值。但是Test()函数中的变量a还是全局变量。

注意:大家以后在开发中,尽量不要让全局变量的名字与局部变量的名字一样。

所以大家,思考以下程序执行的结果:

var a int //变量定义在函数外面

func Test() {

a := 5

a += 1

fmt.Println("Test",a)

}

func main() {

a := 9

Test()

fmt.Println("main",a)

}

5.5.3 总结

  • 在函数外边定义的变量叫做全局变量。

  • 全局变量能够在所有的函数中进行访问。

  • 如果全局变量的名字和局部变量的名字相同,那么使用的是局部变量的

六、项目管理

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

6.1 工作区介绍


通过前面函数的学习,我们能够体会到函数的优势,就是可以将不同的功能放在不同的函数中实现,主函数(main())可以直接调用。这样结构非常的清晰,也非常方面代码的管理。如果我们把所有的代码都写在main()函数中,会出现什么样的情况呢?

代码混乱,非常不容易管理。但是现在我们面临了另外一个问题就是:我们所有自己定义的函数都写在了一个文件中。

如果我们做的项目代码量越来越多,那么该文件会变的非常臃肿,代码也会变得非常难管理。所以,我们在开发中,除了要定义函数,同时还要将代码放在不同的文件中。例如:我们定义了一个UserInfo.go文件,里面包含了用户的添加函数,修改函数,删除函数等操作。

这就涉及到项目的工程管理也就是怎样对项目中的文件进行管理。

为了更好的管理项目中的文件,要求将文件都要放在相应的文件夹中。GO语言规定如下的文件夹如下:

  • src目录:用于以代码包的形式组织并保存Go源码文件。(比如:.go.c.h.s等)。

  • pkg目录:用于存放经由go install命令构建安装后的代码包(包含Go库源码文件)的“.a”归档文件。

  • bin目录:与pkg目录类似,在通过go install命令完成安装后,保存由Go命令源码文件生成的可执行文件。

以上目录称为工作区,工作区其实就是一个对应于特定工程的目录。

目录src用于包含所有的源代码,是Go命令行工具一个强制的规则,而pkgbin则无需手动创建,如果必要Go命令行工具在构建过程中会自动创建这些目录。

6.1.1 标准命令概述

Go语言中包含了大量用于处理Go语言代码的命令和工具。其中,go命令就是最常用的一个,它有许多子命令。这些子命令都拥有不同的功能,如下所示。

  • build:用于编译给定的代码包或Go语言源码文件及其依赖包。

  • clean:用于清除执行其他go命令后遗留的目录和文件。

  • doc:用于执行godoc命令以打印指定代码包。

  • env:用于打印Go语言环境信息。

  • fix:用于执行go tool fix命令以修正给定代码包的源码文件中包含的过时语法和代码调用。

  • fmt:用于执行gofmt命令以格式化给定代码包中的源码文件。

  • get:用于下载和安装给定代码包及其依赖包(提前安装git或hg)。

  • list:用于显示给定代码包的信息。

  • run:用于编译并运行给定的命令源码文件。

  • install:编译包文件并编译整个程序。

  • test:用于测试给定的代码包。

  • tool:用于运行Go语言的特殊工具。

  • version:用于显示当前安装的Go语言的版本信息。

6.2 创建同级目录


6.2.1 创建src目录,在该目录下创建go源码文件

  1. 在项目文件夹下新建src目录,如下图所示:在这里插入图片描述

我这里是在D盘的Workspace目录下创建的src目录。

  1. src目录下创建不同的go源码文件,如下图所示:在这里插入图片描述

然后在src目录下创建main.go文件和test.go文件(注意:这个两个文件是在同一个目录下面,都是在src目录下面)。

main.go文件下的代码如下所示:

package mian

import "fmt"

func main () {

fmt.Println("main")

}

test.go文件下的代码如下所示:

package main //必须与main.go必须是一个包

import "fmt"

func Test () {

fmt.Println("Test")

}

这也是一个简单的打印语句。

我们现在已经完成两个文件代码的编写,接下来的问题是,我们怎样在main.go文件中的入口函数main()中调用test.go文件中的Test()函数呢?这就需要设置环境变量GOPATH属性。如果要实现不同文件中函数的调用,必须设置GOPATH,否则,即使文件处于同一工作目录(工作区)下,也是无法完成调用的。

6.2.2 GOPATH设置

GOPATH设置的具体步骤如下:

在这里插入图片描述在这里插入图片描述

最后再配置完成后,可以测试一下是否配置成功。

在这里插入图片描述

6.2.3 在main.go文件中完成对test.go文件中函数的调用

在这里插入图片描述

最后编译执行。

注意:同一个目录下不能定义不同的package

6.3 创建不同级目录


在上一小节中,将不同的go源代码文件都放在了同一个目录下面,如果将go源代码文件放在不同的目录下面应该怎样进行处理呢?

6.3.1 步骤:

  1. 新建项目目录:

如下图所示:在这里插入图片描述

CmsProject目录下面,创建src目录,在src目录下面创建如下目录与文件。在这里插入图片描述

main.go定义的是入口函数main()

userinfo文件夹下定义的是user.go文件。

在这里插入图片描述

user.go文件中的代码如下:

//不同目录 包名不一样

package userinfo

import "fmt"

func Add(){

fmt.Println("添加用户信息")

}

main.go文件中的代码如下:

package main

import "fmt"

import "userinfo" //注意导包

func main(){

fmt.Println("main")

userinfo.Add() //通过包名.函数进行调用

}

导入用户的包,然后通过用户调用添加的方法,通过以上两个文件中的代码,可以总结出如下几点:

  1. 不同目录,包名不一致(自定义包)。

  2. main.go中调用user.go中的方法时,一定要导包,并且调用的方式是:包名.函数名 的方式。

  3. GoLand中设置GOPATH在这里插入图片描述

会打开如下的窗口,然后进行设置。

在这里插入图片描述

注意:user.go文件中的函数名首字母必须大写,如果改成小写在main.go中无法进行调用

  1. 再添加其他文件和函数:

这种不同级目录应用,在以后的项目开发中使用频率非常高。例如:上面我们的案例中,可以将用户管理的操作放在userinfo目录下,商品管理模块可以再定义一个目录,例如:product.go

如下图所示:在这里插入图片描述

product.go中的代码如下:在这里插入图片描述

main.go中的代码如下:在这里插入图片描述

6.3.2 关于包的问题

包就是一个标识,标志着代码是来自哪儿,对代码进行管理。

所以,在main()函数中要使用相应的函数,必须进行导包,然后根据包名去调用相应的函数。

通过上面的代码,我们也能够体会出“包”的优势,就是可以在userinfo包中定义名叫Add()方法,在product包中也可以定义Add()方法,但是在main()函数中进行调用时,通过包名进行调用,就可以很清楚Add()方法来自哪个包,不会造成混乱,和名称的冲突。并且相关的功能代码,放在一个包中,可以很好的被复用。例如:可以在userinfo包中使用product,如下图所示:

在这里插入图片描述

但是我们创建的的自定义包最好放在GOPATHsrc目录下,在Go语言中,代码包中的源码文件名可以是任意的。但是,这些任意名称的源码文件都必须以包声明语句作为文件中的第一行,每个包都对应一个独立的名字空间。

包中成员以名称⾸字母⼤⼩写决定访问权限:

  • public: 首字母大写,可以被包外访问。

  • private: 首字母小写,仅可以被包内访问。

注意:同一个目录下不能定义不同的package

6.3.3 导包的问题

在上面的案例中,要使用包,必须要进行导入,可以通过关键字进行import进行导入,它会告诉编译器你想引用该包内的代码。

如果导入的是标准库中的包(GO语言自带,例如:”fmt”包)会在安装Go的位置找到。Go开发者创建的包会在GOPATH环境变量指定的目录里查找。所以,import关键字的作用就是查找包所在的位置。

如果编译器查遍GOPATH也没有找到要导入的包,那么在试图对程序执行run或者build的时候就会出错。

注意:如果导入包之后,未调用其中的函数或者类型将会报出编译错误。在这里插入图片描述

我们常规的导包方式是用import关键字一个个导入。(Goland会自动帮我们导入包)

例如:

在这里插入图片描述

表示导入三个包,有GO语言自带的包,也有我们自定义的包。但是,这种写法可能比较麻烦,所以为了简化也可以采用如下的方式进行导包:

在这里插入图片描述

这种方式,使用的频率是非常高的。

七、复合类型

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

复合类型:

| 类型 | 名称 | 长度 | 默认值 | 说明 |

| --- | --- | --- | --- | --- |

| pointer | 指针 | | nil | |

| array | 数组 | | 0 | |

| slice | 切片 | | nil | 引⽤类型 |

| map | 字典 | | nil | 引⽤类型 |

| struct | 结构体 | | | |

7.1 数组


如果要存储班级里所有学生的数学成绩,应该怎样存储呢?可能有同学说,通过定义变量来存储。但是,问题是班级有80个学生,那么要定义80个变量吗?

像以上情况,最好是通过数组的方式来存储。

所谓的数组:是指一系列同一类型数据的集合。

7.1.1 数组定义

var a [10]int

数组定义也是通过var关键字,后面是数组的名字a,长度是10,类型是整型。表示:数组a能够存储10个整型数字。也就是说,数组a的长度是10。

我们可以通过len()函数测试数组的长度,如下所示:

var a [10]int

fmt.Println(len(a))

输出结果为10。

当定义完成数组a后,就在内存中开辟了10个连续的存储空间,每个数据都存储在相应的空间内,数组中包含的每个数据被称为数组元素(element),一个数组包含的元素个数被称为数组的长度。

注意:数组的长度只能是常量。以下定义是错误的:

var n int = 10

var a [n]int

会报错。

7.1.2 数组赋值

数组定义完成后,可以对数组进行赋值操作。数组是通过下标来进行操作的,下标的范围是从0开始到数组长度减1的位置。

在这里插入图片描述

var a[10] int表示的范围是a[0],a[1],a[2].......,a[9]

  1. 第一种赋值方法:在这里插入图片描述

  2. 第二种赋值方法

第一种赋值方式比较麻烦,可以使用如下所示:

var a [10]int

for i := 0; i < 10; i++ {

a[i] = i + 1

}

for i := 0; i < 10; i++ {

fmt.Println(a[i])

}

第一次循环i等于1,然后赋值给a[i]也就是a[1],然后a[1]也刚好等于1,然后循环十次。

  1. 使用len()函数方式赋值:

对上面的程序,进行如下的修改:

var a [10]int

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

a[i] = i + 1

}

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

fmt.Println(a[i])

}

同上一样的道理,虽然改成了len(a)但是其实他的值就是10,而且这样更加方便,你也不用刻意去知道他的长度是多少。

7.1.3 数组数据输出

可以使用range。如下所示:

for i,data := range a {

//fmt.Println("下标:",i)

fmt.Println("元素值:",data)

}

i变量存储的是数组的下标,data变量存储的是数组中的值。

  1. 输出值,不输出下标:

如果只想输出数组中的元素值,不希望输出下标,可以使用匿名变量:

for _,data := range a {

//fmt.Println("下标:",i)

fmt.Println("元素值:",data)

}

上面的案例中,首先完成了数组的赋值,然后再输出数组中的值。但是,如果定义完成数组后,没有赋值,直接输出会出现什么样的问题呢?

var a [10]int

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

fmt.Println(a[i])

}

a数组中的元素类型是整型,定义完成后,直接输出,结果全部是0。

  1. 不同类型数组的默认值:

var a [10]float64 // 如果不赋值,直接输出,结果默认全部是0

var a [10]string // 如果不赋值,直接输出,结果默认全部是空字符

var a [10]bool // 如果不赋值,直接输出,结果默认全部是false.

7.1.4 数组初始化

之前我们是定义数组,然后再完成数组的赋值。其实,在定义数组时,也可以完成赋值,这种情况叫做数组的初始化。

具体案例如下:

// 数组初始化

// 1.全部初始化

var a [5]int = [5]int{1,2,3,4,5}

fmt.Println(a)

// 在定义好一个数组的同时就给他赋值

// 自动推导

b := [5]int{1,2,3,4,5}

fmt.Println(b)

// 部分初始化

// 没有初始化的部分 默认为0

c := [5]int{1,2,3}

fmt.Println(c)

// 12300

// 指定某个元素初始化

d := [5]int{2:10,4:20}

fmt.Println(d)

// 其中2的值是10,4的值是20,其他的都为0

// ... 通过初始化确定长度

f:=[...]int{1,2,3}

fmt.Println(len(f))

// 3

7.2 切片


7.2.1 切片(slice)概念

在讲解切片(slice)之前,大家思考一下数组有什么问题?

  1. 数组定义完,长度是固定的。例如:

var num [5]int = [5]int{1,2,3,4,5}

定义的num数组长度是5,表示只能存储5个整型数字,现在向数组num中追加一个数字,这时会出错。因为你已经定义死了。

  1. 使用数组作为函数参数进行传递时,如果实参为5个元素的整型数组,那么形参也必须5个元素的整型数组,否则出错。

针对以上两个问题,可以使用切片来进行解决。

切片与数组的区别: 切片与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大,所以可以将切片理解成“动态数组”,但是,它不是数组。

7.2.2 切片与数组区别

通过定义,来比较一下切片与数组的区别。

  1. 先回顾数组的基本定义初始化:a := [5]int{},数组中[]是一个固定的数字,表示长度。定义完后,长度是固定,最多存储5个数字。

  2. 切片的基本定义初始化如下:s:=[]int{}//定义空切片看定义的方式,发现与数组很相似.但是注意:切片中的[]是空的,或者是“…”。切片的长度和容量可以不固定。现在通过程序演示,动态向切片中追加数据

// 初始化切片

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

// 通过append函数向切片中追加数据

s = append(s,5,6,7)

fmt.Println(s)

append()函数,第一个参数表示向哪个切片追加数据,后面表示具体追加的数据。最终输出结果为:

[1 2 3 5 6 7]

7.2.3 切片其它定义方式

  1. 定义空切片:

//声明切片和声明数组一样,只是少了长度,此为空(nil)切片

var s1 []int

  1. 通过make()函数实现

//借助make函数, 格式 make(切片类型, 长度, 容量)

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

7.2.4 切片的长度与容量

长度是已经初始化的空间(以上切片s初始空间默认值都是0)。容量是已经开辟的空间,包括已经初始化的空间和空闲的空间。我们可以通过如下图来理解切片的长度与容量:

在这里插入图片描述

该切片的长度是5(存有数据,注意如果没有赋值,默认值都是0),容量是10,只不过有5个空闲区域。即使没有给切片s赋值,初始化的空间(长度)默认存储的数据都是0。

演示如下:

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

fmt.Println(s)

输出的结果是:

[0 0 0 0 0]

在使用make()函数定义切片时,一定要注意,切片长度要小于容量,例如:

// 以下是错误的

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(p)

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

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

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

var p *int

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

fmt.Println(p)

出现的错误信息如下:

import78.png

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

var a int

var p *int

p = &a //指向变量a

*p = 99

fmt.Println(p)

在该案例中,定义了一个变量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 (p)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(p) //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(p) //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

img img img

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

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

如果你需要这些资料,可以戳这里获取