Golang学习笔记
一、Golang的学习方向及应用领域
学习方向:

Golang的应用领域:
1. 区块链的应用开发

2. 后台的服务系统

3. 云计算/云服务后台应用

二、Golang简介
-
核心开发团队-三个大牛

-
Google 创造 Golang的原因

-
发展历程


我的学习过程使用的是1.9版本,开发工具前期使用‘VSCode’ 熟练后使用‘jetbrains-golang’
-
Go语言的特点



三、Golang的开发工具

现在JetBrains公司已经有了Golang专门的IDE工具了。
开发工具前期我使用‘VSCode’ 熟练后使用‘jetbrains-golang’
四、Windows下搭建Go开发环境-安装和配置SDK
如何配置环境变量:
五、Linux下搭建Go开发环境-安装和配置SDK
-
Linux下安装SDK

如何确定Linux是多少位?

:step1:下载go SDK--》https://golang.google.cn/dl/
wget https://dl.google.com/go/go1.9.2.linux-amd64.tar.gz 或者下载后上传到Ubuntu
step2:将go1.9.2.linux-amd64.tar.gz 拷贝到/opt目录下
cp go1.9.2.linux-amd64.tar.gz /optstep3:到/opt下解压go1.9.2.linux-amd64.tar.gz
cd /opt tar -zxvf go1.9.2.linux-amd64.tar.gz 【解压后就可以看见一个go目录】
step4:查看

-
Linux下配置Golang环境变量
step1:使用root权限修改/etc/profile文件
# go-sdk 2020-02-12 export GOROOT=/opt/go export PATH=${PATH}:$GOROOT/bin export GOPATH=$HOME/goproject
step2:生效配置
source /etc/profile

六、Mac下搭建Go开发环境-安装和配置SDK
七、Hello world!
编写一个hello.go程序,输出“hello world”


八、Golang执行流程
Go 程序开发注意事项:


九、Go语言转意字符


package main import "fmt" func main() { fmt.Println("姓名\t年龄\t籍贯\t住址") fmt.Println("\njoin\t12\t河北\t北京") }
运行结果:

十、Golang变量
-
注意事项



-
声明方式

// golang的变量使用方式1 // 第一种:指定变量类型,声明后若不赋值,使用默认值 // int 的默认值是0 其它数据 var age int age = 20 fmt.Println("age = ", age)

// 第二种:根据值自行判断变量类型(类型推导) var num = 10.11 fmt.Println("num = ", num)

// 第三种:省略var,注意 := 左侧的变量不应该是已经声明过的,否则编译报错 // 下面的方式等价于 var name string name = "tom" name := "tom" fmt.Println("name = ", name)
-
多变量声明
// 该案例演示Golang如何一次性声明多个变量 var num1, num2, num3 int fmt.Println("num1=", num1, "\tnum2=", num2 , "\tnum3=", num3) // 一次性声明多个变量的方式2 ==> age= 100 sum= 1000 name= 名字 var age, sum, name = 100, 1000, "名字" fmt.Println("age=", age, "\tsum=", sum , "\tname=", name) // 一次性声明多个变量的方式3,使用类型推导 ==> hight= 100 weight= 1000 student= 名字 hight, weight, student := 100, 1000, "名字" fmt.Println("hight=", hight, "\tweight=", weight , "\tstudent=", student)
-
全局变量
package main import "fmt" // 定义全局变量 var n1 = 1 var n2 = 100 var boy = "join" // 上面的声明方式,也可以改为一次性声明 var ( n3 = 300 n4 = 400 boy2 = "mary" ) func main() { // 输出全局变量 fmt.Println("n1=", n1, "n2=", n2 , "boy=", boy) fmt.Println("n3=", n3, "n4=", n4 , "boy2=", boy2) }
-
变量的数据类型

结构体:类似于Java中的class
管道:用于高并发
十一、数据类型
func main() { var n1 = 100 // 查看某个变量的数据类型 // fmt.Printf() 可以做格式化输出 fmt.Printf(" n1 的数据类型是: %T \n", n1) // 在程序中查看变量的占用字节大小和数据类型 var n2 int64 = 10 // unsafe.Sizeof() 可以返回变量占用的字节数 fmt.Printf(" n2 的数据类型是: %T n2 占用字节大小是 %d \n", n2, unsafe.Sizeof(n2)) }
-
整数数据类型
有符号整数:

无符号整数:

其它:

使用细节:

-
小数类型/浮点型


// 演示精度丢失:尾数部分可能丢失 var num3 float32 = -123.0000901 var num4 float64 = -123.0000901 fmt.Println("num3 = ", num3, "num4 = ", num4)
使用细节:
// Golang的浮点型默认声明为float64 var num5 = 1.1 fmt.Printf("num5 的数据类型是 %T \n", num5) // 浮点型声明:十进制形式 如:5.12 .123 num6 := 5.12 num7 := .123 // =>0.123 fmt.Println("num6 = ", num6, "num7 = ", num7) // 浮点型声明:科学计数法形式,e与E一样 num8 := 5.1234e2 // =>5.1234 * 10的2次方 num9 := 5.1234E-2 // =>5.1234 / 10的2次方 fmt.Println("num8 = ", num8, "num9 = ", num9)
-
字符类型(char)

// 演示Golang中字符类型的使用 func main() { var c1 byte = 'a' var c2 byte = '0' // 当我们直接输出byte值,就是输出了对应字符的ASCII码值 // a==> 97 0==>48 fmt.Println("c1 = ", c1) fmt.Println("c2 = ", c2) // 如果我们希望输出对应的字符,需要使用格式化输出 fmt.Printf("c1 = %c c2 = %c \n", c1, c2) // var c3 byte = '黄' // 会报错:./main.go:19:16: constant 40644 overflows byte // 因为byte范围是0~255,所以换一个范围大的类型即可 var c3 int = '黄' fmt.Printf("c3 = %c c3对应的ASCII码值 = %d \n", c3, c3) }
使用细节:
// 可以直接给某个变量赋一个数字,然后按格式输出%c,会输出数字对应的Unicode字符 var c4 int = 22269 //22269->国 fmt.Printf("c4 = %c \n", c4) //字符类型可以直接进行运算,相当于一个整数,运算时按ASCII码值运算 var n1 = 10 + 'a' //10 + 97 = 107 fmt.Println("n1 = ", n1)
字符类型本质讨论:

-
布尔类型(bool)


-
字符串类型(string)

使用细节:

View Codepackage main import ( "fmt" ) // 演示Golang中string类型的使用 func main() { // string的基本使用 var address string = "北京长城!" fmt.Println(address) // string的注意事项 var str string = "hello world!!!" // str[0] = 'y' // 报错:./main.go:14:9: cannot assign to str[0] 不能修改 fmt.Println(str) // 字符串的两种表现形式(1)双引号,(2)反引号,在esc下面那个键 str2 := "abc\nabc" //双引号会转意 fmt.Println(str2) // 使用反引号``,以字符串的原生形式输出,包括换行和特殊字符,可以防止攻击 str3 := ` package main import "fmt" func main() { // golang的变量使用方式1 // 第一种:指定变量类型,声明后若不赋值,使用默认值 // int 的默认值是0 其它数据 var age int age = 20 fmt.Println("age = ", age) // 第二种:根据值自行判断变量类型(类型推导) var num = 10.11 fmt.Println("num = ", num) // 第三种:省略var,注意 := 左侧的变量不应该是已经声明过的,否则编译报错 // 下面的方式等价于 var name string name = "tom" name := "tom" fmt.Println("name = ", name) } ` fmt.Println(str3) // 字符串拼接方式 var str4 = "hello" + "world" str4 += " hhhha!" fmt.Println(str4) }
-
基本数据类型的默认值


使用细节:

-
基本数据类型的相互转换

基本语法:

// 演示Golang中基本数据类型的转换 func main() { var i int32 = 100 // 希望将i =》 float var n1 float32 = float32(i) var n2 int8 = int8(i) var n3 int64 = int64(i) //低精度-》高精度 fmt.Printf("i=%v, n1=%v, n2=%v, n3=%v\n", i, n1, n2, n3) }

使用细节:

// 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化 fmt.Printf("i type is %T\n", i) //int32 // 在转换中,比如将int64转为int8【-128~127】,编译不会报错 // 只是转换的结果按溢出处理,和我们希望的不一样 var num1 int64 = 999999 var num2 int8 = int8(num1) //63 fmt.Println("num2 = ", num2)



-
基本数据类型和string类型的相互转换
在程序开发中,我们经常将基本数据类型转换成string或者将string转成基本数据类型
基本数据类型转string方式1:fmt.Sprintf("%参数", 表达式) 【个人习惯这个,灵活】
// 演示Golang中基本数据类型转成string func main() { var num1 int = 99 var num2 float64 = 23.456 var b bool = true var myChar byte = 'h' var str string //空的string // 使用第一种方式转换 fmt.Sprintf方法 // 前面的%参数详情:https://studygolang.com/pkgdoc 下的fmt str = fmt.Sprintf("%d", num1) fmt.Printf("str type is %T str = %q \n", str, str) str = fmt.Sprintf("%f", num2) fmt.Printf("str type is %T str = %q \n", str, str) str = fmt.Sprintf("%t", b) fmt.Printf("str type is %T str = %q \n", str, str) str = fmt.Sprintf("%c", myChar) fmt.Printf("str type is %T str = %q \n", str, str) }

基本数据类型转string方式2:使用 strconv 包的函数
package main import ( "fmt" "strconv" ) // 演示Golang中基本数据类型转成string func main() { // 使用第二种方式转换 使用 strconv 包的函数 // 详情:https://studygolang.com/pkgdoc 下的strconv var num3 int = 99 var num4 float64 = 23.456 var b2 bool = true var str string //空的string str = strconv.FormatInt(int64(num3), 10) fmt.Printf("str type is %T str = %q \n", str, str) // 说明: 'f'--格式;10--表示小数位保留10位;64--表示这个小数是float64 str = strconv.FormatFloat(num4, 'f', 10, 64) fmt.Printf("str type is %T str = %q \n", str, str) str = strconv.FormatBool(b2) fmt.Printf("str type is %T str = %q \n", str, str) }

Itoa函数:可以直接将int类型转为string类型
// strconv包中还有一个Itoa函数:可以直接将int类型转为string类型 var num5 int64 = 567 str = strconv.Itoa(int(num5)) fmt.Printf("str type is %T str = %q \n", str, str)
string转基本数据类型的方式:使用 strconv 包的函数
package main import ( "fmt" "strconv" ) // 演示Golang中string转成基本数据类型 func main() { var str string = "true" var b bool // strconv.ParseBool(str)函数会返回两个值 (value bool, err error) // 因为只想获取value,不想获取err,所以使用‘_’忽略 b, _ = strconv.ParseBool(str) fmt.Printf("b type is %T b = %v \n", b, b) var str2 string = "1234560" var num1 int64 //将str2转成10进制的;转成64位的;使用‘_’忽略err num1, _ = strconv.ParseInt(str2 ,10, 64) fmt.Printf("num1 type is %T num1 = %v \n", num1, num1) // 因为go默认转换回来的都是64位,不论是int还是float,如果我们不想要那么大,只能自己转 var num2 int num2 = int(num1) fmt.Printf("num2 type is %T num2 = %v \n", num2, num2) var str3 string = "123.456" var f1 float64 //将str3转成转成64位的;使用‘_’忽略err f1, _ = strconv.ParseFloat(str3 , 64) fmt.Printf("f1 type is %T f1 = %v \n", f1, f1) }

-
指针类型


package main import "fmt" // 演示Golang中指针类型 func main() { // 基本数据类型在内存中的布局 var i int = 10 // i的地址是什么,&i fmt.Println("i的地址是", &i) // ptr是一个指针变量,类型是*int,本身的值是&i var ptr *int = &i fmt.Printf("ptr = %v\n", ptr) // 指针本身也有地址 fmt.Println("ptr的地址是", &ptr) // 取出ptr指向的值 fmt.Printf("ptr指向的值%v\n", *ptr) }
使用细节:
案例演示:

var num int = 120 fmt.Println("num的值是:", num) fmt.Println("num的地址是:", &num) var ptr *int fmt.Println("ptr的地址是:", &ptr) ptr = &num fmt.Println("ptr的值是:", ptr) // 修改ptr指向的值,即修改num的值 *ptr = 1540 fmt.Println("修改后num的值是:", num)



-
值类型和引用类型

特点:

-
标识符的命名规范
标识符的概念:

标识符的命名规则:


注意事项:



十二、算术运算符
运算符的基本介绍:
-
算术运算符


案例演示:

注意事项:

-
关系运算符(比较运算符)

案例演示:

注意事项:

-
逻辑运算符

案例演示:


注意事项:

-
赋值运算符

案例演示:


注意事项:

-
位运算符

-
其它运算符

案例演示:

特别说明:

-
运算符的优先级

特别说明:


十三、获取用户终端输入
- 使用 fmt.Scanln()
View Codepackage main import ( "fmt" ) // 要求:可以从控制台接收用户信息【姓名,年龄,薪水,是否通过考试】 func main() { // 声明变量 var name string var age byte var sal float32 var isPass bool // 方式1:fmt.Scanln() fmt.Println("请输入姓名 ") // 当程序执行到 fmt.Scanln(&name) 时,程序会停止在这,等待用户输入并回车 fmt.Scanln(&name) fmt.Println("请输入年龄 ") fmt.Scanln(&age) fmt.Println("请输入薪水 ") fmt.Scanln(&sal) fmt.Println("请输入是否通过考试 ") fmt.Scanln(&isPass) fmt.Printf("姓名是 %v \n年龄是 %v \n薪水是 %v \n是否通过考试 %v \n ", name, age, sal, isPass) }
- 使用 fmt.Scanf()
View Codepackage main import ( "fmt" ) // 要求:可以从控制台接收用户信息【姓名,年龄,薪水,是否通过考试】 func main() { // 声明变量 var name string var age byte var sal float32 var isPass bool fmt.Println("请输入您的姓名,年龄,薪水,是否通过考试;使用空格隔开! ") fmt.Scanf("%s %d %f %t",&name, &age, &sal, &isPass ) fmt.Printf("姓名是 %v \n年龄是 %v \n薪水是 %v \n是否通过考试 %v \n ", name, age, sal, isPass) }
十四、进制
(1)二进制:0,1,满2进1
- 在golang中,不能直接使用一个二进制表示一个整数,可以用八进制、十进制和十六进制表示,它沿用了c语言的特点
(2)十进制:0-9,满10进1
(3)八进制:0-7,满8进1,以数字0开头
(4)十六进制:0-9及A-F,满16进1,以0x或0X开头,此处A-F不区分大小写,例如0x21AF+1=0x21B0
- 案例演示:
package main import ( "fmt" ) // 进制的基本使用 func main() { var i int = 5 // 二进制输出 fmt.Printf("%b \n", i) // 八进制:0-7,满8进1,以数字0开头 var j int = 011 fmt.Println("j=", j) // 十六进制:0-9及A-F,满16进1,以0x或0X开头,此处A-F不区分大小写 var k int = 0x11 fmt.Println("k=", k) }

十五、位运算
-
原码、反码、补码

-
golang中的3个位运算符:“按位与&”、“按位或|”、“按位异或^”
代码演示:按位与& : 两边全为1,则为1,否则为0
按位或| : 有一个1,则为1,否则为0
按位异或^: 两边一个为0 一个为1,则为1,否则为0
package main import ( "fmt" ) // 位运算的演示:在计算机运算时,都是以补码的方式来进行的 func main() { // 2&3 //2的补码 0000 0010 因为2是整数,所以前面为0000,负数则为1000 //3的补码 0000 0011 // ===》0000 0010 = 2 “两边全为1,则为1,否则为0” 【 而且结果还是补码,但是正数的反、补码==原码】 fmt.Println("2&3 =",2&3) // 2|3 //2的补码 0000 0010 因为2是整数,所以前面为0000,负数则为1000 //3的补码 0000 0011 // ===》0000 0011 = 3 “有一个1,则为1,否则为0” 【 而且结果还是补码,但是正数的反、补码==原码】 fmt.Println("2|3 =",2|3) // 2^3 //2的补码 0000 0010 因为2是整数,所以前面为0000,负数则为1000 //3的补码 0000 0011 // ===》0000 0001 = 1 “两边一个为0 一个为1,则为1,否则为0” 【 而且结果还是补码,但是正数的反、补码==原码】 fmt.Println("2^3 =",2^3) // -2^2 //-2的原码 1000 0010 负数反码=(原码符号位不变,其它取反)1111 1101 负数补码=(反码+1)1111 1110 // 1111 1110 //2的补码 0000 0010 // ===》1111 1100【 结果还是补码,要转回原码】结果的反码=1111 1011 所以原码=1000 0100 = -4 fmt.Println("-2^2 =",-2^2) }

-
golang中的2个移位运算符:“右移运算符>>”、“左移运算符<<”
代码演示:右移运算符>> : 低位溢出,符号位不变,并用符号位补溢出的高位
左移运算符<< : 符号位不变,低位补0
package main import ( "fmt" ) // 移位运算的演示:在计算机运算时,都是以补码的方式来进行的 // 右移运算符>> : 低位溢出,符号位不变,并用符号位补溢出的高位 // 左移运算符<< : 符号位不变,低位补0 func main() { // 1>>2 1的原、反、补码=0000 0001;所以1右移2位=0000 0000 = 0 fmt.Println("1>>2 =",1>>2) // 1<<2 1的原、反、补码=0000 0001;所以1左移2位=0000 0100 = 4 fmt.Println("1<<2 =",1<<2) // -1>>2 -1的原码=1000 0001、-1反码=(原码符号位不变,其它取反)1111 1110、-1补码=(反码+1)1111 1111;所以-1右移2位得出结果的补码=1111 1111 //补码1111 1111的反码 = 1111 1110 原码=1000 0001=-1 fmt.Println("-1>>2 =",-1>>2) // -1<<2 -1的原码=1000 0001、-1反码=(原码符号位不变,其它取反)1111 1110、-1补码=(反码+1)1111 1111;所以-1左移2位得出结果的补码=1111 1100 //补码1111 1100的反码 = 1111 1011 原码=1000 0100=-4 fmt.Println("-1<<2 =",-1<<2) }

十六、程序流程控制
在程序中,程序运行的流程控制决定程序是如何执行的,是我们必须掌握的,主要有三大流程控制语句。
1) 顺序控制
2) 分支控制
3) 循环控制
-
1、顺序控制
程序从上到下逐行地执行,中间没有任何判断和跳转。

-
2、分支控制
分支控制就是让程序有选择执行。有下面三种形式
1) 单分支
2) 双分支
3) 多分支1、单分支



2、双分支


3、多分支控制


4、switch 分支控制

基本语法:



switch 的使用的注意事项和细节:
1) case/switch 后是一个表达式( 即:常量值、变量、一个有返回值的函数等都可以)2) case 后的各个表达式的值的数据类型,必须和 switch 的表达式数据类型一致
3) case 后面可以带多个表达式,使用逗号间隔。比如 case 表达式 1, 表达式 2 ...
4) case 后面的表达式如果是常量值(字面量),则要求不能重复
5) case 后面不需要带 break , 程序匹配到一个 case 后就会执行对应的代码块,然后退出 switch,如果一个都匹配不到,则执行 default
6) default 语句不是必须的.
7) switch 后也可以不带表达式,类似 if --else 分支来使用。
8) switch 后也可以直接声明/定义一个变量,分号结束,不推荐。
9) switch 穿透-fallthrough ,如果在 case 语句块后增加 fallthrough ,则会继续执行下一个 case,也叫 switch 穿透

10) Type Switch:switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际指向的变量类型

5、switch 和 if 的比较

-
3、循环控制--for 循环控制
程序从上到下逐行地执行,中间没有任何判断和跳转。
1、快速入门
var str string = "hello world!" for i := 0; i < len(str); i++ { fmt.Printf("%c \n", str[i]) }
2、for 循环的基本语法


3、for 循环执行流程

4、for 循环的使用注意事项和细节讨论程
1) 循环条件是返回一个布尔值的表达式
2) for 循环的第二种使用方式
for 循环判断条件 {
//循环执行语句
}
将变量初始化和变量迭代写到其它位置
3) for 循环的第三种使用方式
for {
//循环执行语句
}
上面的写法等价 for ; ; {} 是一个无限循环, 通常需要配合 break 语句使用
4) Golang 提供 for-range 的方式,可以方便遍历字符串和数组



-
4、循环控制-while 和 do..while 的实现
Go 语言没有 while 和 do...while 语法,这一点需要同学们注意一下,如果我们需要使用类似其它语言(比如 java / c 的 while 和 do...while ),可以通过 for 循环来实现其使用效果。
1、while 循环的实现


2、do..while 的实现


-
5、跳转控制语句-break
break 语句用于终止某个语句块的执行,用于中断当前 for 循环或跳出 switch 语句。
1、案例演示
随机生成1-100的一个数,直到生成99为止,看看一共用了几次?
package main import ( "fmt" "math/rand" "time" ) //break例子 func main() { //在go中,需要随机生成一个随机数种子,否则返回的值总是固定的 // time.Now().Unix() : 返回一个从1970:01:01的0时-分0秒到现在的秒数(单位秒) //rand.Seed(time.Now().Unix()) // 随机生成1-100的整数, rand.Intn(100)是生成[0, 100) //n := rand.Intn(100) + 1 //fmt.Println("n = ", n) var count int = 0 for { // UnixNano() : 返回一个从1970:01:01的0时-分0秒到时间t的纳秒数(单位纳秒) rand.Seed(time.Now().UnixNano()) n := rand.Intn(100) + 1 fmt.Println("n = ", n) count++ if (n == 99) { break } } fmt.Println("生成99共用了 ", count) }2、break注意事项和细节说明
以 for 循环使用 break 为例

-
6、跳转控制语句-continue
continue 语句用于结束本次循环,继续执行下一次循环。
continue 语句出现在多层嵌套的循环语句体中时。可以通过标签指明要跳过的是哪一层循环 , 这个和前面的 break 标签的使用的规则一样。1、continue注意事项和细节说明
以 for 循环使用 continue 为例

-
7、跳转控制语句-goto
1) Go 语言的 goto 语句可以无条件地转移到程序中指定的行。
2) goto 语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能。
3) 在 Go 程序设计中一般不主张使用 goto 语句,以免造成程序流程的混乱,使理解和调试程序都产生困难。
1、goto 循环的基本语法
goto label .. . label: statement
2、for 循环执行流程

3、快速入门
package main import ( "fmt" ) //goto 例子 func main() { fmt.Println("ok1") goto label1 fmt.Println("ok2") fmt.Println("ok3") label1: fmt.Println("ok4") fmt.Println("ok5") fmt.Println("ok6") }

-
8、跳转控制语句-return
return 使用在方法或者函数中,表示跳出所在的方法或函数。
1) 如果 return 是在普通的函数,则表示跳出该函数,即不再执行函数中 return 后面代码,也可以理解成终止函数。
2) 如果 return 是在 main 函数,表示终止 main 函数,也就是说终止程序。
package main import ( "fmt" ) //return 例子 func main() { for i:=1; i <= 10; i++ { if i == 3 { return } fmt.Println("哇哇哇!", i) } fmt.Println("Hello World!") }

十七、函数、包和错误处理
1、函数
1)基本语法

2)入门案例
func cal(num1 float64, num2 float64) float64 { return num1 + num2 }
3)函数使用的注意事项和细节
1) 函数的形参列表可以是多个,返回值列表也可以是多个。
2) 形参列表和返回值列表的数据类型可以是值类型和引用类型。
3) 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似 public , 首字母小写,只能被本包文件使用,其它包文件不能使用,类似 private
4) 函数中的变量是局部的,函数外不生效
5) 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
6) 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。
7) Go 函数不支持函数重载
8) 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
package main import ( "fmt" ) func getSum(n1 int, n2 int) int { return n1 + n2 } func main() { a := getSum fmt.Printf("a的数据类型是:%T,getSum的数据类型是%T\n", a, getSum) // 等价于 res := getSum(10. 40) res := a(10, 40) fmt.Println("res = ", res) }
9) 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
func getSum(n1 int, n2 int) int { return n1 + n2 } func myFun(funVar func(int, int) int, num1 int, num2 int) int { return funVar(num1, num2) } func main() { res2 := myFun(getSum, 50, 50) fmt.Println("res2 = ", res2) }
10) 为了简化数据类型定义,Go 支持自定义数据类型
基本语法:type 自定义数据类型名 数据类型 // 理解: 相当于一个别名 案例:type myInt int // 这时 myInt 就等价 int 来使用了 案例:type mySum func (int, int) int // 这时 mySum 就等价 一个 函数类型 func (int, int) int
11) 支持对函数返回值命名
// 支持对函数返回值命名,在返回值列表上定义了名称,就可以直接返回,不用 return sum, sub func getSumAndSub(n1 int, n2 int) (sum int, sub int) { sum = n1 +n2 sub = n1- n2 return } func main() { a1, b1 := getSumAndSub(10, 5) fmt.Printf("a1 = %v,b1 = %v\n", a1, b1) }
12) 使用 _ 标识符,忽略返回值
13) Go 支持可变参数
2、包的引出
1) 在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如 main.go中,去使用 utils.go 文件中的函数,如何实现? -》包
2) 现在有两个程序员共同开发一个 Go 项目,程序员 xiaoming 希望定义函数 Cal ,程序员 xiaoqiang也想定义函数也叫 Cal。两个程序员为此还吵了起来,怎么办? -》包
1)打包基本语法
package 包名
2)引入包的基本语法
import "包的路径"
3)包使用的快速入门
4)包使用的注意事项和细节
1) 在给一个文件打包时,该包对应一个文件夹,比如这里的 utils 文件夹对应的包名就是 utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。
2) 当一个文件要使用其它包函数或变量时,需要先引入对应的包
引入方式 1: import "包名" 引入方式 2: import ( "包名" "包名" )
package 指令在 文件第一行,然后是 import 指令。
在 import 包时,路径从 $GOPATH 的src 下开始,不用带 src , 编译器会自动从 src 下开始引入以下是我的环境变量中定义的:
initDemo所在位置:
3) 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的 public ,这样才能跨包访问。比如 utils.go 的
4) 在访问其它包函数,变量时,其语法是 包名.函数名, 比如这里的 main.go 文件中
5) 如果包名较长,Go 支持给包取别名, 注意细节:取别名后,原来的包名就不能使用了说明: 如果给包取了别名,则需要使用别名来访问该包的函数和变量。
6) 在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义
7) 如果你要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main .这个就是一个语法规范,如果你是写一个库 ,包名可以自定义
十八、init 函数
1、函数简介
每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用,也就是说 init 会在 main 函数前被调用。
2、案例说明
package main import ( "fmt" ) // init函数:通常可以在init函数中完成初始化工作 func init() { fmt.Println("init() ....") } func main() { fmt.Println("main() ....") }

3、inti 函数的注意事项和细节
1) 如果一个文件同时包含全局变量定义, init 函数和 main 函数,则执行的流程全局变量定义->init函数->main 函数
2) init 函数最主要的作用,就是完成一些初始化的工作,比如下面的案例
/src/go_code/initDemo/main.gopackage main import ( "fmt" // 引入包 "go_code/initDemo/utils" ) var age = test() // 为了看到全局变量实现被初始化的,我们这里先写这个函数 func test() int { // 1 fmt.Println("全局变量被定义 ....") return 90 } // init函数:通常可以在init函数中完成初始化工作 func init() { // 2 fmt.Println("init() ....") } func main() { // 3 fmt.Println("main() ....") fmt.Println("Age = ", utils.Age, "Name = ", utils.Name) }
/src/go_code/initDemo/utils/utils.gopackage utils import "fmt" // Age和Name是全局变量,在这里初始化,在其它地方被调用 var Age int var Name string func init() { fmt.Println("utils包下的 init() ....") Age = 100 Name = "soldier~" }执行/init/main.go后输出:
4、面试题:案例如果 main.go 和 utils.go 都含有 变量定义,init 函数时,执行的流程又是怎么样的呢?

十九、匿名函数
1、匿名函数简介
Go 支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
2、案例说明
1)使用方式 1:在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次
// 匿名函数使用方式 func main() { // 使用方式 1:在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次 // 演示求两个数的和 res := func (n1 int, n2 int) int { return n1 + n2 }(10, 20) fmt.Println("res = ", res) }

2)使用方式 2:将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
// 匿名函数使用方式 func main() { // 使用方式 2:将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数 a := func(n1 int, n2 int) int { return n1 +n2 } res2 := a(5, 5) fmt.Println("res2 = ", res2) res3 := a(50, 50) fmt.Println("res3 = ", res3) }

3、全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
package main // 使用括号可以导入多个包,定义变量也是 import ( "fmt" ) // 定义多个全局变量 var ( // Fun1就是一个全局匿名函数,注意首字母大写 == java的public Fun1 = func(n1 int, n2 int) int { return n1 * n2 } ) // 匿名函数使用方式 func main() { // 全局匿名函数的使用 res4 := Fun1(100, 100) fmt.Println("res4 = ", res4) }

二十、闭包
1、基本介绍
闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)。
2、案例说明
// 累加器 // AddUpper:方法名 func (int) int:返回值类型列表 func AddUpper() func (int) int { var n int = 10 return func (x int) int { n = n + x return n } } // 闭包案例演示 func main() { // 使用累加器 f := AddUpper() fmt.Println(f(1)) // 11 fmt.Println(f(2)) // 13 fmt.Println(f(3)) // 16 }

对上面代码的说明和总结:
1) AddUpper 是一个函数,返回的数据类型是 fun (int) int
2) 闭包的说明
返回的是一个匿名函数, 但是这个匿名函数引用到函数外的 n(当做一个全局变量来理解即可) ,因此这个匿名函数就和 n 形成一个整体,构成闭包。
3) 大家可以这样理解: 闭包是类, 函数是操作,n 是字段。函数和它使用到 n 构成闭包。
4) 当我们反复的调用 f 函数时,因为 n 只初始化一次,因此每调用一次就进行累计。
5) 我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。
6) 对上面代码的一个修改,加深对闭包的理解
// 累加器 // AddUpper:方法名 func (int) int:返回值类型列表 func AddUpper() func (int) int { var n int = 10 var str string = "hello" return func (x int) int { n = n + x str += string(36) //36 = '$' fmt.Println("str = ", str) // 1.str = "hello$" 2.str = "hello$$" 3.str = "hello$$$" return n } } // 闭包案例演示 func main() { // 使用累加器 f := AddUpper() fmt.Println(f(1)) // 11 fmt.Println(f(2)) // 13 fmt.Println(f(3)) // 16 }
3、闭包的最佳实践
请编写一个程序,具体要求如下
1) 编写一个函数 makeSuffix(suffix string)可以接收一个文件后缀名(比如.jpg),并返回一个闭包
2) 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。
3) 要求使用闭包的方式完成
4) strings.HasSuffix , 该函数可以判断某个字符串是否有指定的后缀。
代码:
package main import ( "fmt" "strings" ) func makeSuffix(suffix string) func (string) string { return func (name string) string { // 如果 name 没有指定后悔,则加上,否则直接返回 if !strings.HasSuffix(name, suffix) { return name + suffix } return name } } // 闭包案例演示 func main() { // 返回一个闭包 f2 := makeSuffix(".jpg") fmt.Println("文件处理后 = ", f2("winter")) // winter.jpg fmt.Println("文件处理后 = ", f2("soldier.jpg")) // soldier.jpg }

上面代码的总结和说明:
1) 返回的匿名函数和 makeSuffix (suffix string) 的 suffix 变量 组合成一个闭包,因为返回的函数引用到 suffix 这个变量
2) 我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入后缀名,比如 .jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用。
二十一、函数的 defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。
1、入门案例
func sum(n1 int, n2 int) int { // 当执行到defer时,暂不执行,会将defer后面的语句压入到独立的栈(defer栈) // 当函数执行完毕后,再从defer栈,按照后进先出的方式出栈,执行 defer fmt.Println("ok1 n1= ", n1) //defer3 defer fmt.Println("ok2 n2= ", n2) //defer2 res := n1 + n2 defer fmt.Println("ok3 res= ", res) //defer1 return res } func main() { res := sum(10, 20) fmt.Println("res= ", res) }

2、defer 的注意事项和细节
1) 当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中[为了方便了;理解,暂时称该栈为 defer 栈], 然后继续执行函数下一个语句。
2) 当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制),所以我们看到前面案例输出的顺序。
3) 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。请看一段代码:
func sum(n1 int, n2 int) int { // 当执行到defer时,暂不执行,会将defer后面的语句压入到独立的栈(defer栈) // 当函数执行完毕后,再从defer栈,按照后进先出的方式出栈,执行 defer fmt.Println("ok1 n1= ", n1) //defer3 defer fmt.Println("ok2 n2= ", n2) //defer2 // 添加一个增加语句 n1++ //=11 n2++ //=21 res := n1 + n2 //=32 defer fmt.Println("ok3 res= ", res) //defer1 return res } func main() { res := sum(10, 20) fmt.Println("res= ", res) }
3、defer 的最佳实践
defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。看下面模拟代码:
说明:
1) 在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源), 可以执行 defer file.close() defer connect.close()
2) 在 defer 后,可以继续使用创建资源。
3) 当函数完毕后,系统会依次从 defer 栈中,取出语句,关闭资源。
4) 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。
二十二、函数参数传递方式
函数参数传递方式有两种:
1) 值传递
2) 引用传递值类型参数默认就是值传递,而引用类型参数默认就是引用传递。
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。
1、值类型和引用类型
1) 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
2) 引用类型:指针、slice 切片、map、管道 chan、interface 等都是引用类型
2、值传递和引用传递使用特点
1)值类型参数默认就是值传递:变量直接存储值,内存通常在栈中分配

2)引用类型参数默认就是引用传递:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址是,该地址对应的数据空间就变成一个垃圾,由GC来回收。

3)如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用

二十三、字符串常用的系统函数
1)统计字符串的长度,按字节==》len(str)
func main() { // 统计字符串的长度,按字节 len(str) // golang的编码统一为utf-8(ascii的字符(字母和数字)占一个字符,汉字占3个) str := "hello黄" fmt.Println("str len = ", len(str)) //8 }

2)字符串遍历,同时处理有中文的问题==》r := []rune(str)
func main() { str2 := "hello北京" // 字符串遍历,同时处理有中文的问题==》r := []rune(str) r := []rune(str2) for i := 0; i < len(r); i++ { fmt.Printf("字符 = %c\n", r[i]) } }

3)字符串转整数==》n, err := strconv.Atoi("12")
import ( "fmt" "strconv" ) func main() { // 字符串转整数==》n, err := strconv.Atoi("12") n, err := strconv.Atoi("hello") if err != nil { fmt.Println("转换错误", err) } else { fmt.Println("转换的结果是", n) } }

可利用这个特性来进行参数校验,比如校验输入的QQ号是否全部为数字
4)整数转字符串==》str := strconv.Itoa(12345)
import ( "fmt" "strconv" ) func main() { // 整数转字符串==》str := strconv.Itoa(12345) str := strconv.Itoa(12345) fmt.Printf("str的类型是%T, 值是%v\n", str, str) }

5)字符串 转 []byte==》var bytes = []byte("hello go")
func main() { // 字符串 转 []byte==》var bytes = []byte("hello go") var bytes = []byte("hello go") fmt.Println("bytes = ", bytes) }

6)[]byte 转 字符串==》 str := string([]byte{97, 98, 99})
func main() { // []byte 转 字符串==》 str = string([]byte{97, 98, 99}) var str = string([]byte{97, 98, 99}) fmt.Println("str = ", str) }

7)10 进制转 2, 8, 16 进制==》str := strconv.FormatInt(123, 2) // 2表示进制数,可换成8 , 16
import ( "fmt" "strconv" ) func main() { str := strconv.FormatInt(123, 2) fmt.Println("123对应的二进制数数:", str) str = strconv.FormatInt(123, 16) fmt.Println("123对应的十六进制数数:", str) }

8)查找子串是否在指定的字符串中==》strings.Contains("seafood", "foo") //true
import ( "fmt" "strings" ) func main() { res := strings.Contains("seafood", "foo") fmt.Println("res = ", res) }

9)统计一个字符串有几个指定的子串==》strings.Count("ceheese", "e") //4
import ( "fmt" "strings" ) func main() { count := strings.Count("ceheese", "e") fmt.Println("count = ", count) }

10)不区分大小写的字符串比较(==符号是区分字母大小写的 )==》fmt.Println(strings.EqualFold("abc", "Abc")) // true
11)返回子串在字符串第一次出现的 index 值,如果没有返回-1==》strings.Index("NLT_abc", "abc") // 4
12)返回子串在字符串最后一次出现的 index,如没有返回-1==》strings.LastIndex("go golang", "go")
13)将指定的子串替换成另外一个子串==》strings.Replace("go go hello", "go", "go 语言", n) n 可以指定你希望替换几个,如果 n=-1 表示全部替换
import ( "fmt" "strings" ) func main() { str := "go go hello" strNew := strings.Replace(str, "go", "soldier", -1) fmt.Printf("str = %v\nstrNew = %v\n", str, strNew) }

14)按照指定的某个字符,双引号下的","为分割标识 , 将一个字符串拆分成字符串数组==》strings.Split("hello,wrold,ok", ",")
import ( "fmt" "strings" ) func main() { strArr := strings.Split("hello,wrold,ok", ",") for i := 0; i < len(strArr); i++ { fmt.Printf("strArr[%v] = %v\n", i, strArr[i]) } fmt.Printf("strArr = %v\n", strArr) }

15)将字符串的字母进行大小写的转换==》strings.ToLower("Go") // go strings.ToUpper("Go") // GO
import ( "fmt" "strings" ) func main() { // 将字符串的字母进行大小写的转换==》strings.ToLower("Go") // go strings.ToUpper("Go") // GO str := "golang Hello" fmt.Println("转成小写:", strings.ToLower(str)) fmt.Println("转成大写:", strings.ToUpper(str)) }

16)将字符串左右两边的空格去掉==》strings.TrimSpace(" tn a lone gopher ntrn ")
17)将字符串左右两边指定的字符去掉==》strings.Trim("! hello! ", " !")")// ["hello"] //将左右两边 !和 " "空格去掉
18)将字符串左边指定的字符去掉==》strings.TrimLeft("! hello! ", " !")// ["hello"] //将左边 ! 和 " "空格去掉
19)将字符串右边指定的字符去掉==》strings.TrimRight("! hello! ", " !")// ["hello"] //将右边 ! 和 " "空格去掉
20)判断字符串是否以指定的字符串开头==》strings.HasPrefix("ftp://192.168.10.1", "ftp") // true
21)判断字符串是否以指定的字符串结束==》strings.HasSuffix("NLT_abc.jpg", "abc") //false
二十四、时间和日期相关函数
1) 时间和日期相关函数,需要导入 time 包
import ( "fmt" "time" )
2) time.Time 类型,用于表示时间
func main() { // 1、获取当前时间 now := time.Now() fmt.Printf("现在的时间是:%v\nnow的类型是:%T\n", now, now) }

3) 如何获取到其它的日期信息
func main() { // 1、获取当前时间 now := time.Now() fmt.Printf("现在的时间是:%v\nnow的类型是:%T\n", now, now) // 2、通过now可以获取年月日时分秒 fmt.Println("现在的年份是:", now.Year()) fmt.Printf("现在的月份是:%v,类型是:%T\n", now.Month(), now.Month()) fmt.Println("现在的月份是:", int(now.Month())) fmt.Printf("现在是%v号\n", now.Day()) fmt.Printf("现在是%v点(时)\n", now.Hour()) fmt.Printf("现在是%v分\n", now.Minute()) fmt.Printf("现在是%v秒\n", now.Second()) }

4) 格式化日期时间
方式1:使用 Printf 或者 SPrintf
func main() { // 1、获取当前时间 now := time.Now() // 格式化时间 fmt.Printf("现在是:%v-%v-%v %v:%v:%v\n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) dateStr := fmt.Sprintf("现在是:%v-%v-%v %v:%v:%v", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) fmt.Println("dateStr = ", dateStr) }

方式2:使用 time.Format()
func main() { // 1、获取当前时间 now := time.Now() // 格式化时间的第二种方式 fmt.Println(now.Format("2006-01-02 15:04:05")) fmt.Println(now.Format("2006/01/02 15:04:05")) fmt.Println(now.Format("2006/01")) fmt.Println(now.Format("2006-01-02")) fmt.Println(now.Format("15:04:05")) }
对上面代码的说明
"2006/01/02 15:04:05"或者"2006-01-02 15:04:05" 这个字符串的各个数字是固定的,必须是这样写。
"2006/01/02 15:04:05"这个字符串各个数字可以自由的组合,这样可以按程序需求来返回时间和日期

5)时间的常量
const (
Nanosecond Duration = 1 //纳秒
Microsecond = 1000 * Nanosecond //微秒
Millisecond = 1000 * Microsecond //毫秒
Second = 1000 * Millisecond //秒
Minute = 60 * Second //分钟
Hour = 60 * Minute //小时
)
常量的作用:在程序中可用于获取指定时间单位的时间,比如想得到 100 毫秒:
100 * time. Millisecon
6)结合 Sleep 来使用一下时间常量
func main() { // 需求1:每隔1秒打印一个数字,直到100退出 // 需求2:每隔0.1秒打印一个数字,直到100退出 i := 0 for { i++ fmt.Println(i) // 休眠 // time.Sleep(time.Second) time.Sleep(time.Millisecond* 100) if i == 100 { break } } }
7)time 的 Unix 和 UnixNano 的方法

func main() { // 1、获取当前时间 now := time.Now() // time 的 Unix 和 UnixNano 的使用 fmt.Printf("Unix的时间戳=%v\tUnixNano的时间戳=%v\n", now.Unix(), now.UnixNano()) }

8)练习:编写一段代码来统计 函数test 执行的时间
package main import ( "fmt" "time" "strconv" ) func test() { str := "" for i:=0; i<10000; i++ { str += "hello" + strconv.Itoa(i) } } func main() { // 执行test函数前先获取当前时间戳 start := time.Now().Unix() test() end := time.Now().Unix() fmt.Printf("执行test函数共使用了%v秒\n", end - start) }

二十五、内置函数
Golang 设计者为了编程方便,提供了一些函数,这些函数可以直接使用,我们称为 Go 的内置函数。文档:https://studygolang.com/pkgdoc -> builtin
1) len:用来求长度,比如 string、array、slice、map、channel
2) new:用来分配内存,主要用来分配值类型,比如 int、float32,struct...返回的是指针
func main() { num1 := 100 fmt.Printf("num1的的类型是:%T,值是:%v,地址是:%v\n", num1, num1, &num1) num2 := new(int) //*int // num2的类型==》*int // num2的值==》地址 0xc42001c0f8(这个地址是系统分配的) // num2的地址==》地址 0xc42000c030(这个地址是系统分配的) // num2指向的值==》100 *num2 = 100 fmt.Printf("num2的的类型是:%T,值是:%v,地址是:%v,指向的值是:%v\n", num2, num2, &num2, *num2) }
上面代码对应的内存分析图:
3) make:用来分配内存,主要用来分配引用类型,比如 channel、map、slice。
二十六、错误处理
看一段代码,引出错误处理
func test() { num1 := 10 num2 := 0 res := num1 /num2 fmt.Println("res = ", res) } func main() { test()
fmt.Println("这里执行吗...") }
对上面代码的总结:
1) 在默认情况下,当发生错误后(panic) ,程序就会退出(崩溃.)
2) 如果我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件,短信。。。)
3) 这里引出我们要将的错误处理机制
1、基本说明
1) Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try...catch...finally 这种处理。
2) Go 中引入的处理方式为:defer, panic, recover
3) 这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理
2、使用defer+recover来捕获和处理错误
func test() { // 使用defer+recover来捕获和处理错误 defer func() { //defer声明并执行一个匿名函数 fmt.Println("defer比正常代码后执行!在return之后执行,如果存在多个defer声明,则defer之间遵循后进先出原则") err := recover() //recover()内置函数,可以捕获到异常 if err != nil { //说明捕获到了异常 fmt.Println("err = ", err) } }() num1 := 10 num2 := 0 fmt.Println("正常代码比defer先执行!") res := num1 /num2 fmt.Println("res = ", res) } func main() { test() fmt.Println("捕获异常后,这里就可以正常执行了...") }

3、错误处理的好处
进行错误处理后,程序不会轻易挂掉,如果加入预警代码,就可以让程序更加的健壮。

4、自定义错误
Go 程序中,也支持自定义错误, 使用 errors.New 和 panic 内置函数。
1) errors.New("错误说明") , 会返回一个 error 类型的值,表示一个错误
2) panic 内置函数 ,接收一个 interface{}类型的值(也就是任何值了)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序.
import ( "fmt" "errors" ) func main() { test2() fmt.Println("mian()的其它业务代码...") } // 返回一个error类型数据 func readFile(fileName string) (err error) { if fileName == "config.ini" { // 读取文件... return nil } else { // 返回一个自定义错误 return errors.New("读取文件错误!") } } func test2() { err := readFile("config.xxxx") if err != nil { // 如果读取文件发生错误,就输出这个错误并定位panic的位置,并终止程序 panic(err) } fmt.Println("test()的其它业务代码...") }

二十七、数组与切片(slice)
1、数组介绍与快速入门
数组可以存放多个同一类型数据。数组也是一种数据类型,在 Go 中,数组是值类型。
func main() { // 1、定义一个数组 var hens [7]float64 // 2、给数组赋值,下标从0开始 hens[0] = 3.0 hens[1] = 1.0 hens[2] = 3.0 hens[3] = 4.0 hens[4] = 2.0 hens[5] = 5.0 hens[6] = 2.0 // 3、遍历数组,求出和 total := 0.0 for i := 0; i < len(hens); i++ { total += hens[i] } // 4、求出平均数 avg := fmt.Sprintf("%.2f", total / float64(len(hens))) fmt.Printf("总和是:%v,平均数是:%v\n", total, avg) }

2、数组定义和内存布局
1)数组的定义
var 数组名 [数组大小]数据类型
var a [5]int
赋初值 a[0] = 1 a[1] = 30 ....
2)数组在内存布局(重要)

对上图的总结:
1)当定义数组后,其实数组的各个元素的默认值都是0(int类型、float类型)、" "(string类型)、false(bool类型)
2)数组的地址可以通过数组名来获取 &intArr
3)数组的第一个元素的地址,就是数组的首地址
4)数组的各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8 int32->4...
func main() { var intArr [3]int //int占8个字符 // 当定义数组后,其实数组的各个元素的默认值都是0 fmt.Println(intArr) intArr[0] = 10 intArr[1] = 20 intArr[2] = 30 fmt.Println(intArr) fmt.Printf("intArr的地址是:%p,intArr[0]的地址是:%p,intArr[1]的地址是:%p,intArr[2]的地址是:%p\n", &intArr, &intArr[0], &intArr[1], &intArr[2]) }
3、数组的使用
1)入门案例:从终端循环输入 5 个成绩,保存到 float64 数组,并输出.
func main() { var score [5]float64 for i := 0; i < len(score); i++ { fmt.Printf("请输入第%d个学生的成绩:\n", i+1) fmt.Scanln(&score[i]) } // 变量打印 for i := 0; i < len(score); i++ { fmt.Printf("score[%d] = %v\n", i+1, score[i]) } }

2)四种初始化数组的方式(重要)
func main() { // 四种初始化数组的方式 var intArr01 [3]int = [3]int{1, 2, 3} fmt.Println("intArr01 =", intArr01) var intArr02 = [3]int{5, 6, 7} fmt.Println("intArr02 =", intArr02) // 这里的 [...] 是规定的写法 var intArr03 = [...]int{8, 9, 10} fmt.Println("intArr03 =", intArr03) var intArr04 = [...]int{1: 800, 0: 900, 2: 1000} fmt.Println("intArr04 =", intArr04) // 类型推导 strArr := [...]string{1: "love~", 0: "**soldier", 2: "qiuxiang!"} fmt.Println("strArr =", strArr) }

4、数组的遍历
1)常规变量,就是前面的使用for循环遍历
2)for-range 结构遍历
这是 Go 语言一种独有的结构,可以用来遍历访问数组的元素。
func main() { // 演示for-range遍历 strArr := [...]string{1: "love~", 0: "**soldier", 2: "qiuxiang!"} for i, v := range strArr { fmt.Printf("i = %v, v = %v\t", i, v) fmt.Printf("strArr[%d] = %v\n", i, strArr[i]) } for _, value := range strArr { fmt.Printf("value = %v\n", value) } }

5、数组使用的注意事项和细节
1) 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化

2) var arr []int这时 arr 就是一个 slice 切片,切片后面讲解
3) 数组中的元素可以是任何数据类型,包括值类型和引用类型(&),但是不能混用。
4) 数组创建后,如果没有赋值,有默认值(零值)
数值类型(int、float)数组:默认值为 0
字符串类型(string)数组:默认值为 ""
bool类型数组: 默认值为 false
5) 数组下标必须在指定范围内使用,否则报 panic:数组越界,比如
var arr [5]int则有效下标为 0-4
6) Go 的数组属于值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响:

7 ) 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
func main() { // 演示引用传递,改变数组值 arr := [...]int{0: 10, 1: 20, 2:30} fmt.Println("arr =", arr) test(&arr) fmt.Println("引用传递后,arr =", arr) } func test(arr *[3]int) { // !!注意写法 (*arr)[0] = 88 }

8) 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度,看下面案例
6、切片的基本介绍与快速入门
先看一个需求:我们需要一个数组用于保存学生的成绩,但是学生的个数是不确定的,请问怎么办?解决方案:-》使用切片。
1) 切片的英文是 slice
2) 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
3) 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
4) 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
5) 切片定义的基本语法:
var 切片名 []类型 比如:var a [] int
func main() { // 演示切片的基本使用 var arr [5]int = [...]int{1, 11, 33, 66, 99} // 声明/定义一个切片 // slice是切片名 // arr[1: 3] 表示slice引用到intArr这个数组 // 引用intArr数组的起始下标为1,最后下标为3(不包括3) slice := arr[1: 3] fmt.Println("arr =", arr) fmt.Println("slice的元素是:", slice) fmt.Println("slice的个数是:", len(slice)) // 切片的容量可以动态变化 fmt.Println("slice的容量是:", cap(slice)) }

7、切片在内存中形式(重要)

对上面的分析图总结:
1. slice 的确是一个引用类型
2. slice 从底层来说,其实就是一个数据结构(struct 结构体)type slice struct { ptr *[2]int len int cap int }
8、切片的使用
1)方式1:定义一个切片,然后让切片去引用一个已经创建好的数组,比如前面的案例就是这样的
func main() { // 演示切片的基本使用 var arr [5]int = [...]int{1, 11, 33, 66, 99} // 声明/定义一个切片 // slice是切片名 // arr[1: 3] 表示slice引用到intArr这个数组 // 引用intArr数组的起始下标为1,最后下标为3(不包括3) slice := arr[1: 3] fmt.Println("arr =", arr) fmt.Println("slice的元素是:", slice) fmt.Println("slice的个数是:", len(slice)) // 切片的容量可以动态变化 fmt.Println("slice的容量是:", cap(slice)) }
2)方式 2:通过 make 来创建切片
make在前面有提到过:https://www.cnblogs.com/HuangJie-sol/p/12297878.html#_label24
var 切片名 []type = make([]type, len, [cap])
参数说明:
type: 就是数据类型
len : 大小
cap :指定切片容量,可选, 如果你分配了 cap,则要求 cap>=len(必须)
func main() { // 演示使用make声明切片 // 5 : slice大小 10 :指定切片容量 var slice []float64 = make([]float64, 5, 10) slice[1] = 10 slice[3] = 20 // 对于切片,必须make使用 fmt.Println(slice) fmt.Println("slice的size = ", len(slice)) fmt.Println("slice的cap = ", cap(slice)) }

对上面代码的小结:
1) 通过 make 方式创建切片可以指定切片的大小和容量
2) 如果没有给切片的各个元素赋值,那么就会使用默认值:
int , float=> 0
string =>””
bool =>false3) 通过 make 方式创建的切片对应的数组是由 make 底层维护,对外不可见,即只能通过 slice 去访问各个元素.
3)方式 3:定义一个切片,直接就指定具体数组,使用原理类似 make 的方式
func main() { // 方式 3:定义一个切片,直接就指定具体数组,使用原理类似 make 的方式 var strSlice []string = []string{"**soldier", "love~", "qiuxiang!"} fmt.Println(strSlice) fmt.Println("strSlice的size = ", len(strSlice)) //3 fmt.Println("strSlice的cap = ", cap(strSlice)) //? }

4)方式 1 和方式 2 的区别(面试)
9、切片的遍历
切片的遍历和数组一样,也有两种方式:
for 循环常规方式遍历
for-range 结构遍历切片
10、切片的使用的注意事项和细节
1) 切片初始化时 var slice = arr[startIndex:endIndex]
说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])
2) 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长
var slice = arr[0:end] 可以简写 var slice = arr[:end]
var slice = arr[start:len(arr)] 可以简写: var slice = arr[start:]
var slice = arr[0:len(arr)] 可以简写: var slice = arr[:]
3) cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素
4) 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用
5) 切片可以继续切片
func main() { var arr [5]int = [...]int{10, 20, 30, 40, 50} slice := arr[1: 4] //==20 30 40 for index, value := range slice { fmt.Printf("slice[%v] = %v\t", index, value) } fmt.Println() slice2 := slice[1: 2] //==30 // 因为arr、slice、slice2 指向的数据空间是同一个,因此arr[2]、slice[1]都会随之改变 slice2[0] = 100 fmt.Println("slice = ", slice) fmt.Println("slice2 = ", slice2) fmt.Println("arr = ", arr) }

6) 用 append 内置函数,可以对切片进行动态追加
func main() { var slice []int = []int{100, 200, 300} slice = append(slice, 400, 500, 600) fmt.Println("slice = ", slice) // 通过append将切片slice追加给slice // ...是必须的,否则会报错 slice = append(slice, slice...) fmt.Println("slice = ", slice) }
对上面代码的小结:
切片 append 操作的底层原理分析:
切片 append 操作的本质就是对数组扩容
go 底层会创建一下新的数组 newArr(安装扩容后大小)
将 slice 原来包含的元素拷贝到新的数组 newArr
slice 重新引用到 newArr
注意 newArr 是在底层来维护的,程序员不可见.
7) 切片的拷贝操作
func main() { var slice []int = []int{1, 2, 3, 4, 5} // 10:切片大小len var slice2 = make([]int, 10) copy(slice2, slice) fmt.Println("slice = ", slice) fmt.Println("slice2 = ", slice2) }

对上面代码的说明:
(1) copy(para1, para2) 参数的数据类型是切片
(2) 按照上面的代码来看, slice 和 slice2 的数据空间是独立,相互不影响,也就是说 将slice[0]为任何数(999),slice2[0] 仍然是 1
8) 关于拷贝的注意事项
说明: 上面的代码没有问题,可以运行, 最后输出的是 [1]
9) 切片是引用类型,所以在传递时,遵守引用传递机制。看两段代码,并分析底层原理

11、string 和 slice
1) string 底层是一个 byte 数组,因此 string 也可以进行切片处理
func main() { str := "hello@soldier" // 使用切片获取soldier slice := str[6: ] fmt.Println("slice = ", slice) }
2) string 和切片在内存的形式,以 "abcd" 画出内存示意图
3) string 是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串
4) 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string
rune在前面有用到过:https://www.cnblogs.com/HuangJie-sol/p/12297878.html#_label22
func main() { str := "hello@soldier" arr := []byte(str) arr[0] = 'z' str = string(arr) fmt.Println("str = ", str) // 细节:我们转成 []byte 后可以处理英文和数字,但不能处理中文 // 原因:[]byte 是按字节来处理的,而中文占3个字节,因此会出现乱码 // 解决办法:将 string 转成 []rune 即可,因为 []rune 按字符处理,兼容汉字 arr2 := []rune(str) arr2[0] = '黄' str = string(arr2) fmt.Println("str = ", str) }
二十八、排序和查找
排序的基本介绍:
查找:
在 Golang 中,我们常用的查找有两种:
1) 顺序查找
2) 二分查找(该数组是有序)
1、冒泡排序
func BubbleSort(arr *[5] int) { fmt.Println("排序前,arr =", (*arr)) // 临时变量,用于交换 temp := 0 for i := 0; i < len(*arr) - 1; i++ { for j := 0; j < len(*arr) - 1 - i; j++ { if (*arr)[j] > (*arr)[j+1] { // 交换 temp = (*arr)[j] (*arr)[j] = (*arr)[j+1] (*arr)[j+1] = temp } } } fmt.Println("排序后,arr =", (*arr)) } func main() { // 定义数组 arr := [5]int{24, 69, 80, 13, 57} BubbleSort(&arr) fmt.Println("main.arr =", arr) }

2、查找的案例演示
第一种:顺序查找==》有一个数列:白眉鹰王、金毛狮王、紫衫龙王、青翼蝠王,猜数游戏:从键盘中任意输入一个名称,判断数列中是否包含此名称:
func main() { names := [4]string{"白眉鹰王", "金毛狮王", "紫衫龙王", "青翼蝠王"} var heroName = "" havaHero := false fmt.Println("请输入要查找的英雄名字:") fmt.Scanln(&heroName) // 第一种方式:顺序查找 for index, value := range names { if value == heroName { havaHero = true fmt.Printf("找到了【%v】,下标是:%v\n", value, index) break } } if !havaHero { fmt.Printf("没找到【%v】\n", heroName) } }
第二种:二分查找,前提是数组是有序的
func main() { // 二分查找:前提是有序列表 arr := [6]int{1,8, 10, 89, 1000, 1234} BinaryFind(&arr, 0, len(arr) - 1, 89) } /* 二分查找的思路: 比如我们要查找的数是 findVal 1. arr 是一个有序数组,并且是从小到大排序 2. 先找到 中间的下标 middle = (leftIndex + rightIndex) / 2, 然后让 中间下标的值和 findVal 进行比较: 2.1 如果 arr[middle] > findVal , 就应该向 leftIndex ---- (middle - 1) 2.2 如果 arr[middle] < findVal , 就应该向 middel+1---- rightIndex 2.3 如果 arr[middle] == findVal , 就找到 2.4 上面的 三点 的逻辑会递归执行 3. 想一下,怎么样的情况下,就说明找不到[分析出退出递归的条件!!] if leftIndex > rightIndex { // 找不到.. return .. } */ func BinaryFind(arr *[6]int, leftIndex int, rightIndex int, findVal int) { //判断 leftIndex 是否大于 rightIndex if leftIndex > rightIndex { fmt.Println("找不到") return } //先找到 中间的下标 middle := (leftIndex + rightIndex) / 2 if (*arr)[middle] > findVal { //说明我们要查找的数,应该在 leftIndex --- middel-1 BinaryFind(arr, leftIndex, middle - 1, findVal) } else if (*arr)[middle] < findVal { //说明我们要查找的数,应该在 middel+1 --- rightIndex BinaryFind(arr, middle + 1, rightIndex, findVal) } else { //找到了 fmt.Printf("找到了,下标为%v \n", middle) } }
二十九、二维数组
1、使用方式 1: 先声明/定义,再赋值
func main() { // 定义/声明一个二维数组 var arr [4][6]int // 赋值 arr[0][0] = 1 arr[3][0] = 4 arr[3][5] = 7 // 遍历,使用for-range for _, value1 := range arr { for _, value2 := range value1 { fmt.Print(value2, " ") } fmt.Println() } }
二维数组在内存的存在形式(重点):
func main() { // 定义/声明一个二维数组 var arr [4][6]int fmt.Printf("arr[0]在内存中地址是:%p\n", &arr[0]) fmt.Printf("arr[3]在内存中地址是:%p\n", &arr[1]) fmt.Printf("arr[0][0]在内存中地址是:%p\n", &arr[0][0]) fmt.Printf("arr[1][0]在内存中地址是:%p\n", &arr[1][0]) }
所以,与一维数组一样:https://www.cnblogs.com/HuangJie-sol/p/12297878.html#_label26
2、使用方式 2: 直接初始化
func main() { // 定义/声明并赋值一个二维数组 var arr [2][3]int = [2][3]int{ {1, 2, 3}, {4, 5, 6}} fmt.Println("arr =", arr) }

说明:二维数组在声明/定义时也对应有四种写法[和一维数组类似]
var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}
var 数组名 [大小][大小]类型 = [...][大小]类型{{初值..},{初值..}}
var 数组名 = [大小][大小]类型{{初值..},{初值..}}
var 数组名 = [...][大小]类型{{初值..},{初值..}}
三十、map
map 是 key-value 数据结构,又称为字段或者关联数组。var map 变量名 map[keytype]valuetype1)golang 中的 map,key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只包含前面几个类型的 接口, 结构体, 数组;通常 key 为 int 、string;注意: slice, map 还有 function 不可以,因为这几个没法用 == 来判断
2)valuetype 的类型和 key 基本一样,通常为: 数字(整数,浮点数),string,map,struct
3)map 声明的举例:(注意:声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用)
var a map[string]string var a map[string]int var a map[int]string var a map[string]map[string]string
1、案例演示:
func main() { // 声明/定义一个map var a map[string]string // 使用前 必须make给map分配空间,这里分配10个 a = make(map[string]string, 10) a["no1"] = "soldier" a["no2"] = "hello" // map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准 a["no2"] = "qiuxiang" fmt.Println(a) }
1) map 在使用前一定要 make
2) map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准
3) map 的 value 是可以相同的.
4) map 的 key-value 是无序
5) make 内置函数数目
2、map的其它定义使用方式
func main() { heros := make(map[int]string) heros[1] = "奥特曼" heros[2] = "小怪兽" fmt.Println(heros) cities := map[int]string{ 1: "soldier", 2: "qiuxiang", } fmt.Println(cities) }

3、map 的增删改查操作
1)map 增加和更新
// 如果 key 还没有,就是增加,如果 key 存在就是修改 map["key"] = value
2)map 删除:
// delete 是一个内置函数,如果 key 存在,就删除该 key-value,如果 key 不存在,不操作,但是也不会报错 delete(map,"key")
如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除,可以遍历一下 key, 逐个删除或者 map = make(...),make 一个新的,让原来的成为垃圾,被 gc 回收
func main() { heros := make(map[int]string) heros[1] = "奥特曼" heros[2] = "小怪兽" fmt.Println(heros) cities := map[int]string{ 1: "soldier", 2: "qiuxiang", } fmt.Println(cities) // 演示全部删除 // 方式一:遍历 for key, _ := range heros { delete(heros, key) } fmt.Println("全部删除后heros =", heros) // 方式二:make 一个新的,让原来的成为垃圾,被 gc 回收 cities = make(map[int]string) fmt.Println("全部删除后cities =", cities) }
3)map 查找:
func main() { heros := make(map[int]string) heros[1] = "奥特曼" heros[2] = "小怪兽" // 演示查找 value, ok := heros[1] if ok { fmt.Println("heros中存在key = 1,value =", value) } else { fmt.Println("heros中不存在key = 1") } }

4、map 切片
切片的数据类型如果是 map,则我们称为 slice of map,map 切片,这样使用则 map 个数就可以动态变化了。
1)案例演示
func main() { // 1、声明/定义一个map切片,我理解成Java的json串 heros := make([]map[string]string, 2) //准备放入两个英雄 // 2、添加一个英雄信息 if heros[0] == nil { heros[0] = make(map[string]string, 2) heros[0]["name"] = "奥特曼" heros[0]["age"] = "500" } if heros[1] == nil { heros[1] = make(map[string]string, 2) heros[1]["name"] = "小怪兽" heros[1]["age"] = "200" } fmt.Println(heros) // 如果这里使用heros[2],会越界 因为只声明了2个空间,虽然map切片的个数可以动态变化,但是必须使用append函数 // if heros[2] == nil { ... // 使用append添加第三个英雄 newHero := map[string]string { "name": "孙悟空", "age": "9999+", } heros = append(heros, newHero) fmt.Println(heros) }

5、map 排序
1) golang 中没有一个专门的方法针对 map 的 key 进行排序
2) golang 中的 map 默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样
3) golang 中 map 的排序,是先将 key 进行排序,然后根据 key 值遍历输出即可
import ( "fmt" "sort" ) func main() { // map的排序 map1 := make(map[int]int, 10) map1[10] = 100 map1[1] = 13 map1[4] = 26 map1[8] = 90 fmt.Println(map1) // 如果按照map的key的顺序进行排序输出: // 1、现将map的key放入切片slice中 // 2、对切片排序 // 3、遍历切片,然后按照key来输出map var keys []int for key, _ := range map1 { keys = append(keys, key) } fmt.Println("排序前,keys =", keys) // 排序 sort.Ints(keys) fmt.Println("排序后,keys =", keys) for _,value := range keys { fmt.Printf("map[%v] = %v\n", value, map1[value]) } }

6、map 使用细节
1) map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来的 map

2) map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长 键值对(key-value)
3) map 的 value 也经常使用 struct 类型,更适合管理复杂的数据(比前面 value 是一个 map 更好),比如 value 为 Student 结构体

三十一、面向对象编程
1) Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
2) Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
3) Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
4) Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
5) Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性。
1、快速入门-面向对象的方式(struct)
package main import ( "fmt" ) // 定义一个Cat结构体 type Cat struct { Name string Age int Color string } func main() { // 创建一个cat变量 var cat1 Cat cat1.Name = "小白" cat1.Age = 20 cat1.Color = "绿色" fmt.Println("cat1 =", cat1) }

2、注意事项和细节说明
1) 字段声明语法同变量,示例:字段名 字段类型
2) 字段的类型可以为:基本类型、数组或引用类型
3) 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:
布尔类型是 false ,数值是 0 ,字符串是 ""。
数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]
指针,slice,和 map 的零值都是 nil ,即还没有分配空间。type Person struct { Name string Age int Scores [5]float64 ptr *int //指针 slice []int //切片 map1 map[string]string //map } func main() { var person Person fmt.Println("person =", person) if person.ptr == nil { fmt.Println("ok1") } if person.slice == nil { fmt.Println("ok2") } if person.map1 == nil { fmt.Println("ok3") } // 再次声明,使用slice、map前一定要先make person.slice = make([]int, 10) person.slice[0] = 100 person.map1 = make(map[string]string) person.map1["key1"] = "value1" fmt.Println("person =", person) }4) 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。
3、创建结构体变量和访问结构体字段
1)方式 1-直接声明
var person Person
2)方式 2-{}
type Person struct { Name string Age int } func main() { person2 := Person{"soldier", 20} fmt.Println("person2 =", person2) }

3)方式 3-&指针
type Person struct { Name string Age int } func main() { var p3 *Person = new(Person) // 因为p3是一个指针,因此标准的给字段赋值方式 // (*p3).Name = "soldier1" 也可以写成 p3.Name = "soldier1" (*p3).Name = "soldier1" p3.Name = "soldier2" (*p3).Age = 10 p3.Age = 20 fmt.Println("person2 =", (*p3)) }

4)方式 4-{}加&指针
type Person struct { Name string Age int } func main() { var p4 *Person = &Person{"soldier", 20} // (*p4).Name = "soldier1" 也可以写成 p4.Name = "soldier1" (*p4).Name = "soldier1" p4.Name = "soldier2" (*p4).Age = 10 p4.Age = 20 fmt.Println("p4 =", (*p4)) }

说明:
1) 第 3 种和第 4 种方式返回的是 结构体指针。2) 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名 ,比如 (*person).Name = "tom"
3) 但 go 做了一个简化,也支持 结构体指针.字段名,比如 person.Name = "tom"。更加符合程序员使用的习惯,go 编译器底层 对 person.Name 做了转化 (*person).Name。
4、结构体使用注意事项和细节
1) 结构体的所有字段在内存中是连续的
package main import ( "fmt" ) // 结构体 type Point struct { x int y int } // 结构体 type Rect struct { leftUp, rightDown Point } type Rect2 struct { leftUp, rightDown *Point } func main() { r1 := Rect{Point{1, 2}, Point{3, 4}} // r1有四个int,在内存中是连续分布的 fmt.Printf("r1.leftUp.x的地址 = %p, r1.leftUp.y的地址 = %p, r1.rightDown.x的地址 = %p, r1.rightDown.y的地址 = %p\n", &r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y) // r2有两个*Point ,这两个*Point类型本身的地址也是连续的,但它们指向的地址不一定是连续的 r2 := Rect2{&Point{10, 20}, &Point{30, 40}} // 打印地址 fmt.Printf("r2.leftUp本身的地址 = %p, r2.rightDown本身的地址 = %p\n", &r2.leftUp, &r2.rightDown) // 它们指向的地址不一定连续,这要看系统运行时的分配 fmt.Printf("r2.leftUp指向的地址 = %p, r2.rightDown指向的地址 = %p\n", r2.leftUp, r2.rightDown) }

2) 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)

3) 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转

4) struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
import ( "fmt" "encoding/json" ) type Monster struct { // `json:name` 就是struct 的 tag Name string `json:"name"` Age int `json:"age"` } func main() { // 1、创建一个Monster变量 monster := Monster{"牛魔王", 200} // 2、将monster变量序列化为 json格式字符串 json.Marshal函数使用反射 jsonStr, err := json.Marshal(monster) if err != nil { fmt.Println("json处理错误", err) } fmt.Println("jsonStr = ", string(jsonStr)) }

三十二、工厂模式
一个结构体的声明是这样的:
package model type Student struct { Name string... }因为这里的 Student 的首字母 S 是大写的,如果我们想在其它包创建 Student 的实例(比如 main 包),引入 model 包后,就可以直接创建 Student 结构体的变量(实例)。但是问题来了,如果首字母是小写的,比如 是 type student struct {....} 就不不行了,怎么办---> 工厂模式来解决.
1、使用工厂模式实现跨包创建结构体实例(变量)的案例:
1)如果 model 包的 结构体变量首字母大写,引入后,直接使用, 没有问题
student.go==》
package model // 定义一个结构体,大写 type Student struct { Name string Score float64 }
main.go==》
package main import ( "fmt" "go_code/factory/model" ) func main() { var stu = model.Student { Name : "soldier", Score : 78.0, } fmt.Println(stu) }

2)如果 model 包的 结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决
student.go==》
package model // 定义一个结构体,小写 type student struct { Name string Score float64 } // 因为student结构体首字母小写,因此只能在student.go中使用 // 但是我们可以通过工厂模式解决 跨包使用问题 func NewStudent(name string, score float64) *student { return &student { Name : name, Score : score, } }
main.go==》
package main import ( "fmt" "go_code/factory/model" ) func main() { // student结构体首字母小写,我们可以通过工厂模式来解决 var stu = model.NewStudent("soldier", 88.0) fmt.Println(stu) fmt.Println(*stu) fmt.Println("name =", stu.Name, ", score =", stu.Score) }
2、工厂模式总结:
其实是外部方法,通过调用内部的全局方法(首字母大写),来访问内部的私有变量(首字母小写)
3、如果student结构体中的变量,如Score变为小写score,私有了,该如何调用呢?
student.go==》
package model // 定义一个结构体,小写 type student struct { Name string score float64 } // 因为student结构体首字母小写,因此只能在student.go中使用 // 但是我们可以通过工厂模式解决 跨包使用问题 func NewStudent(name string, score float64) *student { return &student { Name : name, score : score, } } // 如果score字段首字母小写,则其它包不可以直接调用,但可以提供这个方法间接调用 // (stu *student):在go语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接受者的函数,方法和函数只差了一个,那就是方法在 func 和标识符之间多了一个参数 // 传入stu *student 表示可以修改stu的值,传入stu student不会修改stu的值 // 其实这里就相当于java中给一个类添加内部方法,类型是public func (stu *student) GetScore() float64 { return stu.score }
main.go==》
package main import ( "fmt" "go_code/factory/model" ) func main() { // student结构体首字母小写,我们可以通过工厂模式来解决 var stu = model.NewStudent("soldier", 88.0) fmt.Println(stu) fmt.Println(*stu) fmt.Println("name =", stu.Name, ", score =", stu.GetScore()) }
在go中,函数和方法是不一样的,函数在前面有说过,4;一个方法是一个结构体的一种行为,Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct。
三十三、面向对象编程思想-抽象
我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象。
代码实现:
package main import ( "fmt" ) //定义一个结构体 Account type Account struct { AccountNo string Pwd string Balance float64 } //方法 //1. 存款 func (account *Account) Deposite(money float64, pwd string) { //看下输入的密码是否正确 if pwd != account.Pwd { fmt.Println("你输入的密码不正确") return } //看看存款金额是否正确 if money <= 0 { fmt.Println("你输入的金额不正确") return } account.Balance += money fmt.Println("存款成功~~") } //取款 func (account *Account) WithDraw(money float64, pwd string) { //看下输入的密码是否正确 if pwd != account.Pwd { fmt.Println("你输入的密码不正确") return } //看看取款金额是否正确 if money <= 0 || money > account.Balance { fmt.Println("你输入的金额不正确") return } account.Balance -= money fmt.Println("取款成功~~") } //查询余额 func (account *Account) Query(pwd string) { //看下输入的密码是否正确 if pwd != account.Pwd { fmt.Println("你输入的密码不正确") return } fmt.Printf("你的账号为=%v 余额=%v \n", account.AccountNo, account.Balance) } func main() { //测试 account := Account{ AccountNo : "gs1111111", Pwd : "666666", Balance : 100.0, } //这里可以做的更加灵活,就是让用户通过控制台来输入命令... //菜单.... account.Query("666666") account.Deposite(200.0, "666666") account.Query("666666") account.WithDraw(150.0, "666666") account.Query("666666") }
三十四、面向对象编程三大特性-封装
1)Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样。)
2)封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。例如java中实体类字段都是private,但其get/set方法是public的。
1、封装的实现步骤
1) 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
2) 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
3) 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
4) 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值func (var 结构体类型名) SetXxx(参数列表) (返回值列表) { //加入数据验证的业务逻辑 var.字段 = 参数 }func (var 结构体类型名) GetXxx() { return var.age; }特别说明:在 Golang 开发中并没有特别强调封装,这点并不像 Java. 所以提醒学过 java 的朋友,不用总是用 java 的语法特性来看待 Golang, Golang 本身对面向对象的特性做了简化的.
2、快速入门案例
package model import "fmt" // 定义一个结构体 type person struct { Name string age int //其他包不能直接访问... sal float64 } // 写一个工厂模式的函数,相当于构造函数 func NewPerson(name string) *person { return &person { Name : name, } } // 为了访问age 和 sal 使用封装的思想写其get/set方法 func (p *person) SetAge(age int) { if age >0 && age < 150 { p.age = age } else { fmt.Println("年龄范围不正确...") // 给程序员一个默认值,int类型默认值是0 } } func (p *person) GetAge() int { return p.age } func (p *person) SetSal(sal float64) { if sal >= 3000 && sal <= 30000 { p.sal = sal } else { fmt.Println("工资范围不正确...") // 给程序员一个默认值,float64类型默认值是0 } } func (p *person) GetSal() float64 { return p.sal }
package main import ( "fmt" "go_code/fengzhuangDemo/model" ) func main() { p := model.NewPerson("soldier") p.SetAge(21) p.SetSal(10000) fmt.Println(p) fmt.Println("姓名:", p.Name, ",年龄:", p.GetAge(), ",工资:", p.GetSal()) }
文件路径:

运行结果:

三十五、面向对象编程三大特性-继承
1) 现实中两个结构体的字段和方法几乎,但是我们却写了相同的代码, 代码复用性不强
2) 出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展。
3) 解决方法-通过继承方式来解决
基本介绍:
继承可以解决代码复用,让我们的编程更加靠近人类思维。
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 已定义好的结构体的匿名结构体即可。
也就是说:在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
1、基本语法
type Goods struct { Name string Price int } type Book struct { Goods //这里就是嵌套匿名结构体 Goods Writer string }
2、快速入门案例
package main import ( "fmt" ) //编写一个学生考试系统 type Student struct { Name string Age int Score int } //将 Pupil 和 Graduate 共有的方法也绑定到 *Student func (stu *Student) ShowInfo() { fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n", stu.Name, stu.Age, stu.Score) } func (stu *Student) SetScore(score int) { //业务判断 stu.Score = score } //小学生 type Pupil struct { Student //嵌入了 Student 匿名结构体 } //显示他的成绩 //这时 Pupil 结构体特有的方法,保留 func (p *Pupil) testing() { fmt.Println("小学生正在考试中.....") } //大学生, 研究生。。 type Graduate struct { Student //嵌入了 Student 匿名结构体 } //显示他的成绩 //这时 Graduate 结构体特有的方法,保留 func (p *Graduate) testing() { fmt.Println("大学生正在考试中.....") } //代码冗余.. 高中生.... func main() { //当我们对结构体嵌入了匿名结构体使用方法会发生变化 pupil := &Pupil{} pupil.Student.Name = "tom~" pupil.Student.Age = 8 pupil.testing() pupil.Student.SetScore(70) pupil.Student.ShowInfo() graduate := &Graduate{} graduate.Student.Name = "mary~" graduate.Student.Age = 28 graduate.testing() graduate.Student.SetScore(90) graduate.Student.ShowInfo() }
3、继承的深入讨论
继承的好处:
1) 代码的复用性提高了
2) 代码的扩展性和维护性提高了
1) 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。
package main import "fmt" type People struct { Name string age int } func (p *People) SayOk() { fmt.Println("People SayOk", p.Name) } type Woman struct { People } func main() { var woman Woman woman.People.Name = "soldier" woman.People.age = 21 woman.People.SayOk() }

2) 匿名结构体字段访问可以简化
func main() { var woman Woman woman.People.Name = "soldier" woman.People.age = 21 woman.People.SayOk() // 对上面的写法可以简化 woman.Name = "soldier ~~~" woman.age = 22 woman.SayOk() }

对上面的代码小结
(1) 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
(2) 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段
(3) 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有继续查找..如果都找不到就报错.
3) 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
4) 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。

5) 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

6) 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值



三十六、面向对象编程-多重继承
如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
type Goods struct { Name string Price float64 } type Brand struct { Name string Address string } type TV struct { Goods Brand }
多重继承细节说明
1) 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。
2) 为了保证代码的简洁性,建议大家尽量不使用多重继承
三十七、接口(interface)
多态特性主要是通过接口来体现的。
1、快速入门案例
package main import ( "fmt" ) // 声明、定义一个接口 type Usb interface { Start() Stop() } // 定义一个结构体 手机 type Phone struct { } // 让 Phone 实现 Usb 接口的方法 func (p Phone) Start() { fmt.Println("手机开机了...") } func (p Phone) Stop() { fmt.Println("手机关机了...") } // 定义一个结构体 相机 type Camera struct { } func (c Camera) Start() { fmt.Println("相机开始工作。。。") } func (c Camera) Stop() { fmt.Println("相机停止工作。。。") } // 定义一个结构体 电脑 type Computer struct { } //编写一个方法 Working 方法,接收一个 Usb 接口类型变量 //只要是实现了 Usb 接口 (所谓实现 Usb 接口,就是指实现了 Usb 接口声明所有方法) //usb 变量会根据传入的实参,来判断到底是 Phone,还是 Camera func (c Computer) Working (usb Usb) { // 通过 usb 接口调用 usb.Start() usb.Stop() } func main() { // 先创建结构体变量 computer := Computer{} phone := Phone{} camera := Camera{} // 关键点 computer.Working(phone) computer.Working(camera) }

2、接口概念的再说明
interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
3、基本语法
type 接口名 interface { method1(参数列表) 返回值列表 method2(参数列表) 返回值列表 ..... }
实现接口所有方法:
func (t 自定义类型) method1 (参数列表) 返回值列表 { // 方法实现 } func (t 自定义类型) method2 (参数列表) 返回值列表 { // 方法实现 } // .....
小结说明:
1) 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。2) Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字
4、注意事项和细节
1) 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)




































































浙公网安备 33010602011771号