Other
编程
Windows
Linux
Network
Cloud Computing
服务器
Application
Mysql
🗒️Golang
type
status
date
slug
summary
tags
category
icon
password
GOlang吉祥物为金花鼠gordon
Go 设计者思想: 一个问题尽量只有一个解决方法
1、应用领域
- 区块链技术
- 分布式账本技术
- 去中心化、公开透明、每个人均可参与数据库记录
- 服务后端
- 具有C和C++的理念GO语言
- 负载均衡、cache、容错、按条件分流
- 处理日志,数据打包,处理数据,处理高并发
- 分布式
- 云计算
- 调试、分发、监控
做了才会,不是会了在做
编程思路和逻辑思维
2、概述
程序:为了让计算机执行某些操作或解决某个问题而编写的一系列有序指令的集合
为什么要创造GO语言
- 目前主流大部分语言不能合理利用多核多CPU
- 软件系统复杂度越来越高,维护成本也越来越高,缺乏一个足够简洁高效的语言
- 企业运行维护C/C++项目,运行速度很快,但编译很慢,还存在一定的内存泄露需要解决
3、特点
- Go 语言保证了既能到达静态编译语言的安全和性能,又达到了动态语言开发维护的高效率
- 从 C 语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型,调用参数传值,指针等等,也保留了和 C 语言一样的编译执行方式及弱化的指针
- 引入包的概念,用于组织程序结构,Go 语言的一个文件都要归属于一个包,而不能单独存在
- 垃圾回收机制,内存自动回收,不需开发人员管理
- 天然并发 (重要特点)
- 从语言层面支持并发,实现简单
- goroutine,轻量级线程,可实现大并发处理,高效利用多核。
- 基于 CPS 并发模型(Communicating Sequential Processes )实现
- 管道通信机制,形成 Go 语言特有的管道 channel 通过管道 channel , 可以实现不同的 goroute 之间的相互通信
- 函数可以返回多个值
- 新的创新:比如切片 slice、延时执行 defer
5、配置环境变量
6、目录结构处理
7、写一段输出代码编译后运行
编译
go代码 —> 编译 —> 可执行文件 —> 运行 —> 返回结果
编译后可执行文件在相同的操作系统下,可以在任意主机下运行
8、GO开发注意事项
- Go 源文件以 "go" 为扩展名。
- Go 应用程序的执行入口是 main()函数。 这个是和其它编程语言(比如 java/c)
- Go 语言严格区分大小写。
- Go 方法由一条条语句构成,每个语句后不需要分号(Go 语言会在每行后自动加分号),这也体现出 Golang 的简洁性。
- Go 编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个,否 则报错
- go 语言定义的变量或者 import 的包如果没有使用到,代码不能编译通过。
- 大括号都是成对出现的,缺一不可。
9、转义字符
10、常见问题
- 文件名或者路径错误
- 语法错误,违反语法规则
11、注释(comment)
注释提高了代码的阅读性
- 行注释(官方推荐使用此方法)
- 块注释
12、缩进和空白
- 使用gofmt -w hello.go自动编排缩进
- 运算符两边加上空格
- 缩进使用TAB键默认是4个空格
- 一行最长不超过 80 个字符,超过的请使用换行展示,尽量保持格式优雅
13、文档参考
- 包存在位置
- 包源码包存放位置
人 —> 指令 —> 终端 —> 操作系统 —> 解析指令并执行 —> 返回执行结果给终端
14、变量
变量相当于内存中一个数据存储空间的表示,你可以把变量看做是一个房间的门牌号,通过门牌号我们可以找到房间,同样的道理,通过变量名可以访问到变量(值)
变量使用步骤
- 声明变量(也称:定义变量)
- 非变量赋值
- 使用变量
15、变量使用注意事项
- 变量表示内存中的一个存储区域
- 该区域有自己的名称(变量名)和类型(数据类型)
- Golang 变量使用的三种方式
- 第一种:指定变量类型,声明后若不赋值,使用默认值
- 第二种:根据值自行判定变量类型(类型推导)
- 第三种:省略 var, 注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误
- 多变量声明
在编程中,有时我们需要一次性声明多个变量,Golang 也提供这样的语法
- 全局变量
- 如何一次性声明多个全局变量【在 go 中函数外部定义变量就是全局变量】
- 该区域的数据值可以在同一类型范围内不断变化(重点)
- 变量在同一个作用域(在一个函数或者在代码块)内不能重名
- 变量=变量名+值+数据类型,这一点请大家注意,变量的三要素
- Golang 的变量如果没有赋初值,编译器会使用默认值, 比如 int 默认值 0 string 默认值为空串, 小数默认为 0
16、程序中 +号的使用
- 当左右两边都是数值型时,则做加法运算
- 当左右两边都是字符串,则做字符串拼接
17、数据类型
- rune // int32 基本一致,唯一不一样的是rune是以Unicode编码进行存放
- 结构体,可以暂时理解为其他语言中的类class
- 管道是用来做多并发使用
- 切片类似动态数组
- map类似于其他语言中的集合,在go中集合,是一种简单的集合,不向其他其他语言中复杂的符合,仅是一小部分
18、整数类型
- 第一位表示符号位,0表示正数,1表示负数
- 剩下的位数是取值范围,假设位置为n,负数等于2**n,正数为2**n-1
- 为什么正数会减1,0是有正数或者负数,他是的值是0,那么从正数中借一位,那么就会少一数,所以正数减1
19、无符号整形
- 范围,假设位数为n,那么范围则为2**n-1
20、其他整形类型
- int与uint是于操作系统来决定字节
- rune 处理中文字符串比较有用
21、整形使用细节
- Golang 各整数类型分:有符号和无符号,int uint 的大小和系统有关
- Golang 的整型默认声明为 int 型
- 如何在程序查看某个变量的字节大小和数据类型
- Golang 程序中整型变量在使用时,遵守保小不保大的原则,即:在保证程序正确运行下,尽量 使用占用空间小的数据类型。如年龄,使用int64浪费,byte类型即可表示
- bit: 计算机中的最小存储单位。byte:计算机中基本存储单元。[二进制再详细说] 1byte = 8 bit
22、小数类型,也称浮点型
小数类型是用于存放小数的,如 1.2 0.23 -1.911
- 在golang语言中,没有doule,可使用float64来表示
- 关于浮点数在机器中存放形式的简单说明,浮点数=符号位+指数位+尾数位(说明:浮点数都是有符号的)
- 尾数部分可能丢失,造成精度损失。 -123.0000901
- float64精度比float32精度精确
- 若需要保存精确高的数,则选择float64类型
- 浮点型的存储分为三部分:符号位+指数位+尾数位 在存储过程中,精度会有丢失
23、浮点型使用细节
- Golang 浮点类型有固定的范围和字段长度,不受具体 OS(操作系统)的影响。
- Golang 的浮点型默认声明为 float64 类型。
- 浮点型常量有两种表示形式
- 十进制数形式:如:5.12 .512 (必须有小数点)
- 科学计数法形式:如:5.1234e2 = 5.12 * 10 的 2 次方 5.12E-2 = 5.12/10 的 2 次方
- 通常情况下,应该使用 float64 ,因为它比 float32 更精确。[开发中,推荐使用 float64]
24、字符类型
Golang 中没有专门的字符类型,如果要存储单个字符(字母),一般使用 byte 来保存。
字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。也
就是说对于传统的字符串是由字符组成的,而 Go 的字符串不同,它是由字节组成的
- 如果我们保存的字符在 ASCII 表的,比如[0-1, a-z,A-Z..]直接可以保存到 byte
- 如果我们保存的字符对应码值大于 255,这时我们可以考虑使用 int 类型保存
- 如果我们需要安装字符的方式输出,这时我们需要格式化输出,即 fmt.Printf(“%c”, c1)..
25、字符型使用细节
- 字符常量是用单引号('')括起来的单个字符。
例如:var c1 byte = 'a' var c2 int = '中' var c3 byte = '9'
- Go 中允许使用转义字符 '\’来将其后的字符转变为特殊字符型常量。
例如:var c3 char = ‘\n’ // '\n'表示换行符
- Go 语 言 的 字 符 使 用 UTF-8 编 码, 如 果 想 查 询 字 符 对 应 的 utf8 码 值http://www.mytju.com/classcode/tools/encode_utf8.asp 英文字母-1 个字节 汉字-3 个字节
- 在 Go 中,字符的本质是一个整数,直接输出时,是该字符对应的 UTF-8 编码的码值。
- 可以直接给某个变量赋一个数字,然后按格式化输出时%c,会输出该数字对应的 unicode 字符
- 字符类型是可以进行运算的,相当于一个整数,因为它都对应有 Unicode 码.
26、字符类型本质探讨
- 字符型 存储到 计算机中,需要将字符对应的码值(整数)找出来 存储:字符--->对应码值---->二进制-->存储 读取:二进制----> 码值 ----> 字符 --> 读取
- 字符和码值的对应关系是通过字符编码表决定的(是规定好)
- Go 语言的编码都统一成了 utf-8。非常的方便,很统一,再也没有编码乱码的困扰了
27、布尔类型
- 布尔类型也叫 bool 类型,bool 类型数据只允许取值 true 和 false
- bool 类型占 1 个字节。
- bool 类型适于逻辑运算,一般用于程序流程控制
- if 条件控制语句
- for 循环控制语句
28、字符串类型string
字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go
语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本
29、string使用注意事项
- Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本,这样 Golang 统一使用 UTF-8 编码,中文乱码问题不会再困扰程序员
- 字符串一旦赋值了,字符串就不能修改了:在 Go 中字符串是不可变的
- 字符串的两种表示形式
- 双引号, 会识别转义字符
- 反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果
- 字符串拼接方式
- 当一行字符串太长时,需要使用到多行字符串,可以如下处理(注意+号在结尾)
30、基本数据类型的默认值
在 go 中,数据类型都有一个默认值,当程序员没有赋值时,就会保留默认值,在 go 中,默认值
又称零值
基本数据类型默认值如下
31、基本数据类型的相互转换
Golang 和 java / c 不同,Go 在不同类型的变量之间赋值时需要显式转换。也就是说 Golang 中数
据类型不能自动转换(显示转换又转强制转换)
- 基本语法 表达式 T(v) 将值 v 转换为类型 T
- T: 表示数据类型,比如 int32,int64,float32 等等
- v: 需要转换的变量
32、基本数据类型相互转换的注意事项
- Go 中,数据类型的转换可以是从 表示范围小-->表示范围大,也可以 范围大--->范围小
- 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化!
- 在转换中,比如将 int64 转成 int8 【-128---127】 ,编译时不会报错,只是转换的结果是按 溢出处理,和我们希望的结果不一样。 因此在转换时,需要考虑范围
33、基本数据类型和 string 的转换
在程序开发中,我们经常将基本数据类型转成 string,或者将 string 转成基本数据类型
- 基本类型转 string 类型
- 方式 1:fmt.Sprintf("%参数",表达式)【个人习惯这个,灵活】
- 方式 2:使用 strconv 包的函数
可参考文档说明
34、string 类型转基本数据类型
- 使用时 strconv 包的函数
- 返回是int64或者floast64,若需要int32类型,需要做一定显式转换
35、string 转基本数据类型的注意事项
在将 String 类型转成 基本数据类型时,要确保 String 类型能够转成有效的数据,比如 我们可以
把 "123" , 转成一个整数,但是不能把 "hello" 转成一个整数,如果这样做,Golang 直接将其转成 0 ,其它类型也是一样的道理. float => 0 bool => false
- 数值未转换成功会返回0
- bool若未转换成功返回false
36、指针
- 基本数据类型,变量存的就是值,也叫值类型
- 获取变量的地址,用&,比如: var num int, 获取 num 的地址:&num
- 指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值 比如:var ptr *int = &num 指针在内存的布局
- 获取指针类型所指向的值,使用:*,比如:var ptr *int, 使用ptr 获取 ptr 指向的值
示例
37、指针的使用细节
- 值类型,都有对应的指针类型, 形式为 *数据类型
比如 int 的对应的指针就是 *int, float32对应的指针类型就是 *float32, 依次类推
- 值类型包括:基本数据类型 int 系列, float 系列, bool,string 、数组和结构体 struct
38、值类型和引用类型
- 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
- 引用类型:指针、slice 切片、map、管道 chan、interface 等都是引用类型
39、值类型和引用类型的使用特点
- 值类型:变量直接存储值,内存通常在栈中分配
- 引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆 上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由 GC 来回收
- 内存的栈区和堆区示意图
40、标识符的命名规范
- Golang 对各种变量、方法、函数等命名时使用的字符序列称为标识符
- 凡是自己可以起名字的地方都叫标识符
- 标识行的命名规则
- 由26个英文字母大小写,0-9,_组成
- 数字不可以开头
- Golang中严格区分大小写
- 标识符不可以包含空格
- 下划线"_"本身在 Go 中是一个特殊的标识符,称为空标识符。可以代表任何其它的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用
- 不能以系统保留关键字作为标识符(一共有 25 个),比如 break,if 等等…
var num int
var Num int
上述中的 num和Num是不同的变量
var ab c int = 30 此种方式是错误的
41、标识符命名注意事项
- 包名:保持 package 的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,不要和 标准库不要冲突 fmt
- 变量名、函数名、常量名:采用驼峰法
- 如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能 在本包中使用 ( 注:可以简单的理解成,首字母大写是公开的,首字母小写是私有的) ,在 golang 没有public , private 等关键字
42、保留关键字
43、系统的预定义标识符
44、运算符
运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等
运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等
- 算术运算符
- 赋值运算符
- 比较运算符/关系运算符
- 逻辑运算符
- 位运算符
- 其它运算符
45、算术运算符
算术运算符是对数值类型的变量进行运算的,比如:加减乘除。在 Go 程序中使用的非常多
46、算术运算符使用的注意事项
- 对于除号 "/",它的整数除和小数除是有区别的:整数之间做除法时,只保留整数部分而舍弃 小数部分。 例如: x := 19/5 ,结果是 3
- 当对一个数取模时,可以等价 a%b=a-a/b*b , 这样我们可以看到 取模的一个本质运算。
- Golang 的自增自减只能当做一个独立语言使用时,不能这样使用
- Golang 的++ 和 -- 只能写在变量的后面,不能写在变量的前面
如:只有 a++ a-- 没有 ++a --a
- Golang 的设计者去掉 c / java 中的 自增自减的容易混淆的写法,让 Golang 更加简洁,统一。(强 制性的)
示例
47. 关系运算符(比较运算符)
- 关系运算符的结果都是 bool 型,也就是要么是 true,要么是 false
- 关系表达式 经常用在 if 结构的条件中或循环结构的条件中
48. 关系运算符的细节说明
- 关系运算符的结果都是 bool 型,也就是要么是 true,要么是 false。
- 关系运算符组成的表达式,我们称为关系表达式: a > b
- 比较运算符"=="不能误写成 "=" !!
49. 逻辑运算符
用于连接多个条件(一般来讲就是关系表达式),最终的结果也是一个 bool 值
50. 注意事项和细节说明
- && 也叫短路与:如果第一个条件为 false,则第二个条件不会判断,最终结果为 false
- || 也叫短路或:如果第一个条件为 true,则第二个条件不会判断,最终结果为 true
51. 赋值运算符
- 赋值运算符就是将某个运算后的值,赋给指定的变量
52. 赋值运算符的特点
- 运算顺序从右往左
- 赋值运算符的左边 只能是变量,右边 可以是变量、表达式、常量值
- 复合赋值运算符等价于下面的效果
示例 有两个变量,a 和 b,要求将其进行交换,但是不允许使用中间变量,最终打印结果
53. 位运算符
54. 其它运算符说明
示例
55. 特别说明
没有三元运算符,需要使用if else来实现
56. 运算符的优先级
- 运算符有不同的优先级,所谓优先级就是表达式运算中的运算顺序。如右表,上一行运算符总优先于下一行
- 只有单目运算符、赋值运算符是从右向左运算的
- 梳理了一个大概的优先级
- 括号,++, --
- 单目运算
- 算术运算符
- 移位运算
- 关系运算符
- 位运算符
- 逻辑运算符
- 赋值运算符
- 逗号
57. 键盘输入语句
在编程中,需要接收用户输入的数据,就可以使用键盘输入语句来获取。InputDemo.go
- 导入 fmt 包
- 调用 fmt 包的 fmt.Scanln() 或者 fmt.Scanf()
58. 进制
对于整数,有四种表示方式:
- 二进制:0,1 ,满 2 进 1。 在 golang 中,不能直接使用二进制来表示一个整数,它沿用了 c 的特点。
- 十进制:0-9 ,满 10 进 1。
- 八进制:0-7 ,满 8 进 1. 以数字 0 开头表示。
- 十六进制:0-9 及 A-F,满 16 进 1. 以 0x 或 0X 开头表示。 此处的 A-F 不区分大小写
59. 进制转换介绍
- 其它进制转十进制
- 二进制转十进制
- 八进制转十进制
- 十六进制转十进制
- 十进制转换其他进制
- 十进制转二进制
- 十进制转八进制
- 十进制转十六进制
- 二进制转换成八进制、十六进制
- 二进制转八进制(每三位一组)
- 二进制转十六进制(每四位一组)
- 八进制、十六进制转成二进制
- 八进制转换成二进制
- 十六进制转成二进制
60. 位运算
61. 二进制在运算中说明
在计算机的内部,运行各种运算时,都是以二进制的方式来运行
二进制是逢 2 进位的进位制,0、1 是基本算符。
现代的电子计算机技术全部采用的是二进制,因为它只使用 0、1 两个数字符号,非常简单方便,易于用电子方式实现。计算机内部处理的信息,都是采用二进制数来表示的。二进制(Binary)数用 0
和 1 两个数字及其组合来表示任何数。进位规则是“逢 2 进 1”,数字 1 在不同的位上代表不同的值,
按从右至左的次序,这个值以二倍递增。
62. 原码、反码、补码
63. 位运算符和移位运算符
运算是补码,返回结果是原码
Golang 中有 3 个位运算
分别是”按位与&、按位或|、按位异或^,它们的运算规则是:
- 按位与& :两位全为1,结果为 1,否则为 0
- 按位或| : 两位有一个为 1,结果为 1,否则为 0
- 按位异或 ^ :两位一个为 0,一个为 1,结果为 1,否则为 0
示例
Golang 中有 2 个移位运算符:
- >>、<< 右移和左移,运算规则:
- 右移运算符 :低位溢出,符号位不变,并用符号位补溢出的高位
- 左移运算符 <<: 符号位不变,低位补 0
64. 程序流程控制
程序运行的流程控制决定程序是如何执行的,是我们必须掌握的,主要有三大流程控
制语句。
- 顺序控制
- 分支控制
- 循环控制
66. 分支控制
分支控制就是让程序有选择执行。有下面三种形式
- 单分支
- 双分支
- 多分支
单分支控制
示例
双分支控制
双分支只会执行其中的一个分支
示例
多分支控制
- 多分支的判断流程如下:
- 先判断条件表达式 1 是否成立,如果为真,就执行代码块 1
- 如果条件表达式 1 如果为假,就去判断条件表达式 2 是否成立, 如果条件表达式 2 为真,就执行代码块 2
- 依次类推.
- 如果所有的条件表达式不成立,则执行 else 的语句块。
- else 不是必须的。
- 多分支只能有一个执行入口。
嵌套分支
在一个分支结构中又完整的嵌套了另一个完整的分支结构,里面的分支的结构称为内层分支外面的分支结构称为外层分支
一般不会超过3层
示例
switch 分支控制
case/switch 后是一个表达式( 即:常量值、变量、一个有返回值的函数等都可以)
- switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上到下逐一测试,直到匹配为止。
- 匹配项后面也不需要再加 break
上图小结
- switch 的执行的流程是,先执行表达式计算后的值,与case 的表达式进行比较,如果相等,匹配成功,然后执行对应的 case 的语句块,最后退出 switch 控制。
- 如果 switch 的表达式的值没有和任何的 case 的表达式匹配成功,则执行 default 的语句块。最后退出 switch 的控制
- golang 的 case 后的表达式可以有多个,使用 逗号 间隔
- golang 中的 case 语句块不需要写 break , 因为默认会有,即在默认情况下,当程序执行完 case 语句块后,直接退出该 switch 控制结构
switch 的使用的注意事项和细节
- case/switch 后是一个表达式( 即:常量值、变量、一个有返回值的函数等都可以)
- case 后的各个表达式的值的数据类型,必须和 switch 的表达式数据类型一致
- case 后面可以带多个表达式,使用逗号间隔。比如 case 表达式 1, 表达式 2 …
- case 后面的表达式如果是常量值(字面量),则要求不能重复
- case 后面不需要带 break , 程序匹配到一个 case 后就会执行对应的代码块,然后退出 switch,如果一个都匹配不到,则执行 default
- default 语句不是必须的
- switch 后也可以不带表达式,类似 if --else 分支来使用
- switch 后也可以直接声明/定义一个变量,分号结束,不推荐
- switch 穿透-fallthrough ,如果在 case 语句块后增加 fallthrough ,则会继续执行下一个 case,也叫 switch 穿透(应用场景较少,推荐使用 逗号间隔多个表达式)
- Type Switch:switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际指向的变量类型
练习
switch和if区别
- 如果判断的具体数值不多,而且符合整数、浮点数、字符、字符串这几种类型。建议使用 swtich语句,简洁高效。
- 其他情况:对区间判断和结果为 bool 类型的判断,使用 if,if 的使用范围更广。
67. 循环控制
可以实现一段代码循环的执行
for循环
- 语法格式
- 对 for 循环来说,有四个要素:
- 循环变量初始化
- 循环条件
- 循环操作(语句) ,有人也叫循环体。
- 循环变量迭代
- 循环执行顺序
- 执行循环变量初始化,比如 i := 1
- 执行循环条件, 比如 i <= 10
- 如果循环条件为真,就执行循环操作 :比如 fmt.Println(“....”)
- 执行循环变量迭代 , 比如 i++
- 反复执行 2, 3, 4 步骤,直到 循环条件为 False ,就退出 for 循环
for 循环流程图
for 循环的使用注意事项
- 循环条件是返回一个布尔值的表达式
- for 循环的第二种使用方式
- Golang 提供 for-range 的方式,可以方便遍历字符串和数组 (注: 数组的遍历,我们放到讲数组的时候再讲解)
- 如果我们的字符串含有中文,那么传统的遍历字符串方式,就是错误,会出现乱码。原因是传统的对字符串的遍历是按照字节来遍历,而一个汉字在 utf8 编码是对应 3 个字节
练习
while 和 do…while,在GO语言没有此种方法,可以通过for来实现
Go 语言没有 while 和 do...while 语法,这一点需要同学们注意一下,如果我们需要使用类似其它语言(比如 java / c 的 while 和 do...while ),可以通过 for 循环来实现其使用效果
- while 循环的实现
- for 循环是一个无限循环
- break 语句就是跳出 for 循环
- do..while 的实现
- 上面的循环是先执行,在判断,因此至少执行一次。
- 当循环条件成立后,就会执行 break, break 就是跳出 for 循环,结束循环.
多重循环控制(重点、难点)
- 将一个循环放在另一个循环体内,就形成了嵌套循环。在外边的 for 称为外层循环在里面的 for循环称为内层循环。(建议一般使用两层,最多不要超过 3 层,若超过2层㜽么你需要考虑你的逻辑是否合理,且超过3层,会影响代码的效率)
- 实质上,嵌套循环就是把内层循环当成外层循环的循环体。当内层循环的循环条件为 false时,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次的循环
- 外层循环次数为 m 次,内层为 n 次,则内层循环体实际上需要执行 m*n 次
新手编程时两大思想
- 先易后难, 即将一个复杂的问题分解成简单的问题
- 先死后活,先不需要考虑代码写的灵活,后面使用变量修改灵活点
示例
68. break 跳出循环语句
69. continue 跳出控制语句
70. goto 跳转控制语句 (尽量避免使用)
71. retrun 跳转控制语句
return 使用在方法或者函数中,表示跳出所在的方法或函数
- 如果 return 是在普通的函数,则表示跳出该函数,即不再执行函数中 return 后面代码,也可以理解成终止函数
- 如果 return 是在 main 函数,表示终止 main 函数,也就是说终止程序
72. 函数
- 为什么需要使用到函数
- 示例,输入两个数,做运算(+-*/),返回结果
- 为完成某一功能的程序指令(语句)的集合,称为函数。 在 Go 中,函数分为: 自定义函数、系统函数(查看 Go 编程手册)
- 基本语法
73. 包
- 在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如 main.go中,去使用 utils.go 文件中的函数,如何实现? -》包
- 现在有两个程序员共同开发一个 Go 项目,程序员 xiaoming 希望定义函数 Cal ,程序员 xiaoqiang也想定义函数也叫 Cal。两个程序员为此还吵了起来,怎么办? -》包
包的本质实际上就是创建不同的文件夹,来存放程序文件
- 包的基本概念
- go 的每一个文件都是属于一个包的,也就是说 go 是以包的形式来管理文件和项目目录结构的
- 包的三大作用
- 区分相同名字的函数、变量等标识符
- 当程序文件很多时,可以很好的管理项目
- 控制函数、变量等访问范围,即作用域
- 打包基本语法
package 包名
- 引入包的基本语法
import “包的路径”
示例
包使用的注意事项和细节讨论
- 在给一个文件打包时,该包对应一个文件夹,比如这里的 utils 文件夹对应的包名就是 utils, 文件的包名通常和文件所在的文件夹名一致,一般为小写字母
- 当一个文件要使用其它包函数或变量时,需要先引入对应的包
- 引入方式1
- 引入方式2
- package 指令在 文件第一行,然后是 import 指令
- 在 import 包时,路径从 $GOPATH 的src 下开始,不用带 src , 编译器会自动从 src 下开始引入(先去$GOROOT路径查找,在去$GOPATH路径查找,一般不会在GOROOT目录下存放自己定义的包)
- 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言 的 public ,这样才能跨包访问。比如 utils.go 的(首字母大写公有,小写私有)
- 在访问其它包函数,变量时,其语法是 包名.函数名, 比如这里的 main.go 文件中
- 如果包名较长,Go 支持给包取别名, 注意细节:取别名后,原来的包名就不能使用了
说明: 如果给包取了别名,则需要使用别名来访问该包的函数和变量
- 在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义
- 如果你要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main .这个就 是一个语法规范,如果你是写一个库 ,包名可以自定义(在GOPATH目录下进行编译)
74. 函数调用机制
75. 函数递归调用
一个函数在函数体内又调用了本身,我们称为递归调用
- 示例
递归调用小结
- 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
- 函数的局部变量是独立的,不会相互影响
- 递归必须向退出递归的条件逼近,否则就是无限递归,相当于死循环
- 当一个函数执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁
练习
函数使用的注意事项和细节讨论
- 函数的形参列表可以是多个,返回值列表也可以是多个(形参传递变量至函数内,返回值函数运行结果返回给调用者)
- 形参列表和返回值列表的数据类型可以是值类型和引用类型
- 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似 public , 首字母小写,只能被本包文件使用,其它包文件不能使用,类似 private
- 函数中的变量是局部的,函数外不生效
- 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值
- 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用
- Go 函数不支持函数重载(在相同函数名一样,形参个数或者数据类型不同,在其他语言中是认为正确的但在GOLANG中是不支持此种方式,但GOALNG有其他方法可以实现函数重载,如空接口)
- 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
- 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
- 为了简化数据类型定义,Go 支持自定义数据类型 基本语法:type 自定义数据类型名 数据类型 // 理解: 相当于一个别名
- 支持对函数返回值命名
- 使用 _ 标识符,忽略返回值
- Go 支持可变参数
如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后
函数练习
75. init函数
- 每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用,也就是说 init 会在 main 函数前被调用
- 示例
init 函数的注意事项和细节
- 如果一个文件同时包含全局变量定义,init 函数和 main 函数,则执行的流程全局变量定义->init函数->main 函数
- init 函数最主要的作用,就是完成一些初始化的工作,比如下面的案例
- 细节说明: 案例如果 main.go 和 utils.go 都含有 变量定义,init 函数时,执行的流程又是怎么样的呢
被引入文件变量定义 —> 被引入文件init函数 —> main.go变量定义 —> main.go中的init函数 —> main函数
76. 匿名函数
Go 支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用
- 匿名函数使用方式1
- 匿名函数使用方式2
- 全局匿名函数
77. 闭包
闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
- AddUpper是一个函数,返回的数据类型是func(int) int
- 闭包说明
返回的是一个匿名函数,但是此匿名函数引用于函数外的n,因此此函数和n形成一个整体,构成闭包
- 可以将闭包理解成类,函数是操作,n是字段,函数和它使用到 n 构成闭包
- 当我们反复的调用f函数时,因为n是初始化一次,因此每调用一次就进行累计
- 我们需要搞清楚闭包的关键,要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包
- 上面代码小结
- 将.jpg值传递给makeSuffix函数中的suffix变量
- 将desktop.jpg传递给匿名函数中的filename变量
- 做判断,是否以.jpg结尾,若为真返回文件名,若为假则返回文件名+.jpg
- 闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用,传统方式每次都需要重新多传一个值
78. 函数中defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)
defer 的注意事项和细节
- 当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中[为了讲课方便,暂时称该栈为 defer 栈], 然后继续执行函数下一个语句。
- 当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制)
- 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈
defer 实践
defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源(如释放连接和文件句柄很有效)
- 在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源), 可以执行 defer file.Close() defer connect.Close()
- 在 defer 后,可以继续使用创建资源
- 当函数执行完毕后,系统会依次从 defer 栈中,取出语句,关闭资源.
- 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。
79. 函数参数传递方式
80. 变量作用域
- 函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部
- 函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
- 如果变量是在一个代码块,比如 for / if 中,那么这个变量的的作用域就在该代码块
练习
81. 字符串常用的系统函数
内键函数来自builtin包
82. 时间和日期相关函数
83. 内置函数
文档:https://studygolang.com/pkgdoc -> builtin
- len:用来求长度,比如 string、array、slice、map、channel
- new:用来分配内存,主要用来分配值类型,比如 int、float32,struct...返回的是指针
- make:用来分配内存,主要用来分配引用类型,比如 channel、map、slice
84. 错误处理
- 在默认情况下,当发生错误后(panic) ,程序就会退出(崩溃.)
- 当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可 以在捕获到错误后,给管理员一个提示(邮件,短信。。。)
- 这里引出我们要将的错误处理机制
基本说明
- Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try…catch…finally 这种处理
- Go 中引入的处理方式为:defer, panic, recover
- 这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理
使用 defer+recover 来处理错误
进行错误处理后,程序不会轻易挂掉,如果加入预警代码,就可以让程序更加的健壮
自定义错误
Go 程序中,也支持自定义错误, 使用 errors.New 和 panic 内置函数。
- errors.New("错误说明") , 会返回一个 error 类型的值,表示一个错误
- panic 内置函数 ,接收一个 interface{}类型的值(也就是任何值了)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序
练习
85. 数组
数组可以存放多个同一类型数据。数组也是一种数据类型,在 Go 中,数组是值类型
数组定义和内存布局
- 数组定义
- 数组在内存布局
- 数组的地址可以通过数组名来获取 &intArr
- 数组的第一个元素的地址,就是数组的首地址
- 数组的各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8 int32->4...
数组使用
- 访问数组元素
数组名[下标] 比如:你要使用 a 数组的第三个元素 a[2]
- 四种初始化数组的方式
数组的遍历
- 常规遍历
- for-range遍历
这是 Go 语言一种独有的结构,可以用来遍历访问数组的元素
数组使用的注意事项和细节
- 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的 不能动态变化
- var arr []int 这时 arr 就是一个 slice 切片,切片在后面章节
- 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用
- 数组创建后,如果没有赋值,有默认值(零值)
- 数值类型数组:默认值为 0
- 字符串数组:默认值为 ""
- bool 数组: 默认值为 false
- 使用数组的步骤 1. 声明数组并开辟空间 2 给数组各个元素赋值(默认零值) 3 使用数组
- 数组下标是从0开始
- 数组下标必须在指定范围内使用,否则报 panic:数组越界
- 如 var arr [5]int 则有效下标为 0-4
- Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响
- 若想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
- 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度
数组案例
86. 切片
需求:我们需要一个数组用于保存学生的成绩,但是学生的个数是不确定的,请问怎么办?
解决方案:-》使用切片(可以理解为是一个动态的数组,但和数组有一定的区别)
切片的基本介绍
- 切片的英文是 slice
- 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
- 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
- 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
- 切片定义的基本语法:
var 切片名 []类型
比如:var a []int
切片在内存中形式(重要)
更加深入的理解切片,我们画图分析一下切片在内存中是如何布局的,这个是一个非常重要的知识点
- 画出前面的切片内存布局
对上面的分析图总结
- slice 的确是一个引用类型
- slice 从底层来说,其实就是一个数据结构(struct 结构体)
- 引用数组某个元素的地址
- 切片引用的长度
- 切片容量的大小
切片的使用
- 方法1:定义一个切片,然后让切片去引用一个已经创建好的数组
- 方法2:通过 make 来创建切片
基本语法:var 切片名 []type = make([]type, len, [cap])
参数说明:
type: 就是数据类型
len : 大小,长度
cap :指定切片容量,可选, 若分配了 cap, 则要求 cap>=len
上图小结
- 通过 make 方式创建切片可以指定切片的大小和容量(容量可选)
- 如果没有给切片的各个元素赋值,那么就会使用默认值
[int , float=> 0 string =>”” bool => false]
- 通过 make 方式创建的切片对应的数组是由 make 底层维护,对外不可见,只能通过 slice 去访问各个元素
- 方法3:定义一个切片,直接就指定具体数组,使用原理类似 make 的方式
- 方式 1 和方式 2 的区别
切片的遍历
切片的遍历和数组一样,也有两种方式
- for 循环常规方式遍历
- for-range 结构遍历切片
切片的使用的注意事项和细节讨论
- 切片初始化时 var slice = arr[startIndex:endIndex]
说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])
- 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长
- var slice = arr[0:end] 可以简写 var slice = arr[:end] start默认是0
- var slice = arr[start:len(arr)] 可以简写: var slice = arr[start:] end默认是数组 的长度
- var slice = arr[0:len(arr)] 可以简写: var slice = arr[:] start end都省略取整个数组
- cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素
- 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用
- 切片可以继续切片
- 用 append 内置函数,可以对切片进行动态追加
切片 append 操作的底层原理分析:
- 切片 append 操作的本质就是对数组扩容
- go 底层会创建一下新的数组 newArr(安装扩容后大小)
- 将 slice 原来包含的元素拷贝到新的数组 newArr
- slice 重新引用到 newArr
- 注意 newArr 是在底层来维护的,程序员不可见
- 切片的拷贝操作
切片使用 copy 内置函数完成拷贝
- copy(para1, para2) 参数的数据类型是切片
- 按照上面的代码来看, slice5 和 slice6 的数据空间是独立,相互不影响,也就是说 slice5[0]= 999, slice6[0] 仍然是 1
- 关于拷贝的注意事项
以下代码运行无错误,输出为[1]
若slice大小没有a大,运行正常,只会将第一个元素复制,后而的元素发现没有长度存放则放弃
- 切片是引用类型,所以在传递时,遵守引用传递机制。看两段代码,并分析底层原理
string 和 slice
- string 底层是一个 byte 数组,因此 string 也可以进行切片处理
- string 和切片在内存的形式,以 "abcd" 画出内存示意图
- string 是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串
- 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string
切片练习
88. 二维数组
89. map
map 是 key-value 数据结构,又称为字段或者关联数组。类似其它编程语言的集合
基本语法
var map变量名 map[keytype]valuetype
- key 可以是什么类型
golang 中的 map,key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只包含前面几个类型的 接口, 结构体, 数组 (通常 key 为 int 、string)
注意: slice, map 还有 function 不可以,因为这几个没法用 == 来判断
- valuetype 可以是什么类型
valuetype 的类型和 key 基本一样,这里我就不再赘述了
通常为: 数字(整数,浮点数),string,map,struct
示例 声明
- map 在使用前一定要 make
- map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准
- map 的 value 是可以相同的
- map 的 key-value 是无序
- make 内置函数数目
使用方式(3种方法声明)
map 的增删改查操作
- map 增加和更新:
map["key"] = value //如果 key 还没有,就是增加,如果 key 存在就是修改
- map 删除
- 如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除
- 可以遍历一下 key, 逐个删除
- map = make(...),make 一个新的,让原来的成为垃圾,被 gc 回收
delete(map,"key") ,delete 是一个内置函数,如果 key 存在,就删除该 key-value,如果 key 不存在,不操作,但是也不会报错
- map查找
map遍历
相对复杂的 map 遍历:该 map 的 value 又是一个 map
说明:map 的遍历使用 for-range 的结构遍历
map 的长度
map 切片
切片的数据类型如果是 map,则我们称为 slice of map,map 切片,这样使用则 map 个数就可以动态变化了
map 排序
- golang 中没有一个专门的方法针对 map 的 key 进行排序
- golang 中的 map 默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样
- golang 中 map 的排序,是先将 key 进行排序,然后根据 key 值遍历输出即可
map 使用细节
- map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来的 map
- map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长 键值对(key-value)
- map 的 value 也经常使用 struct 类型,更适合管理复杂的数据(比前面 value 是一个 map 更好),比如 value 为 Student 结构体
map 练习
90. 结构体 面向对象
使用已知的知识点来解决
- 使用变量或者数组来解决养猫的问题,不利于数据的管理和维护。因为名字,年龄,颜色都是 属于一只猫,但是这里是分开保存
- 如果我们希望对一只猫的属性(名字、年龄,颜色)进行操作(绑定方法), 也不好处理
- 引出我们要讲解的技术—>结构体
Golang 语言面向对象编程说明
- Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
- Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
- Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等
- Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
- Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性
结构体与结构体变量(实例/对象)的关系示意图
- 将一类事物的特性提取出来(比如猫类), 形成一个新的数据类型, 就是一个结构体。
- 通过这个结构体,我们可以创建多个变量(实例/对象)
- 事物可以猫类,也可以是 Person , Fish 或是某个工具类。。。
示例 使用面向对象的方式(struct)解决养猫问题
结构体和结构体变量(实例)的区别和联系’
- 结构体是自定义的数据类型,代表一类事物.
- 结构体变量(实例)是具体的,实际的,代表一个具体变量
结构体变量(实例)在内存的布局(重要!)
在goalng中,结构体是值类型
声明结构体
字段/属性
- 从概念或叫法上看: 结构体字段 = 属性 = field (即授课中,统一叫字段)
- 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体 的 Name string就是属性
注意事项和细节说明
- 字段声明语法同变量,示例:字段名 字段类型
- 字段的类型可以为:基本类型、数组或引用类型
- 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面一样
布尔类型是 false ,数值是 0 ,字符串是 ""
数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]
指针,slice,和 map 的零值都是 nil ,即还没有分配空间
- 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个 结构体是值类型!!!
创建结构体变量和访问结构体字段
说明
- 第 3 种和第 4 种方式返回的是 结构体指针。
- 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名 ,比如 (*person).Name = "tom"
- 但 go 做了一个简化,也支持 结构体指针.字段名,比如 person.Name = "tom"。更加符合程序员使用的习惯,go 编译器底层 对 person.Name 做了转化 (*person).Name
struct 类型的内存分配机制
注
结构体使用注意事项和细节
- 结构体的所有字段在内存中是连续的
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
- 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
- struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化
91. 方法
在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外( 年龄,姓名..),Person 结构体还有一些行为比如:可以说话、跑步..,通过学习,还可以做算术题。这时就要用方法才能完成
Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct
方法的声明和调用
小述代码小结
- test 方法和 Person 类型绑定
- test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
- func (p Person) test() {}... p 表示哪个 Person 变量调用,这个 p 就是它的副本, 这点和函数传参非常相似
- p 这个名字,有程序员指定,不是固定, 比如修改成 person 也是可以
快速入门
- 给 Person 结构体添加 speak 方法,输出xxx 是一个好人
- 给 Person 结构体添加 jisuan 方法,可以计算从 1+..+1000 的结果, 说明方法体内可以函数一样,进行各种运算
- 给 Person 结构体 jisuan2 方法,该方法可以接收一个数 n,计算从 1+..+n 的结果
- 给 Person 结构体添加 getSum 方法,可以计算两个数的和,并返回结果
- 方法调用
方法的调用和传参机制原理:(重要!)
方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法
- 在通过一个变量去调用方法时,其调用机制和函数一样
- 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
方法声明(定义)
- 参数列表:表示方法输入
- recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type类型
- receiver type : type 可以是结构体,也可以其它的自定义类型
- receiver : type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
- 返回值列表:表示返回的值,可以多个
- 方法主体:表示为了实现某一功能代码块
- return 语句不是必须的
方法的注意事项和细节
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理(在开发中一般使用方式较多,使用指针类型会提交传输效率)
- Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问
- 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出
练习
方法和函数的区别
- 调用方式不一样
函数的调用方式 函数名(实参列表)
方法的调用方式 变量.方法名(实参列表)
- 关于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,简单来说(值类型只能值传递,指针类型只能指针传递)
- 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法
简单来说
值类型 即是值拷贝,若传递的是指针,也是值拷贝,编译器底层做了优化,取指针的数据进行拷贝
指针类型 即指针拷贝,也就是内存中的地址拷贝
最终决定的类型取决于方法传递的类型
不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定
如果是和值类型,比如(p Person) , 则是值拷贝, 如果和指针类型,比如是 (p *Person) 则是地址拷贝
92. 面向对象编程应用实例
- 步骤
- 声明(定义)结构体,确定结构体名
- 编写结构体的字段
- 编写结构体的方法
- 学生案例
- 编写一个 Student 结构体,包含 name、gender、age、id、score 字段,分别为 string、string、int、int、float64 类型。
- 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
- 在 main 方法中,创建 Student 结构体实例(变量),并访问 say 方法,并将调用结果打印输出。
- 小狗案例
- 编写一个 Dog 结构体,包含 name、age、weight 字段
- 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
- 在 main 方法中,创建 Dog 结构体实例(变量),并访问 say 方法,将调用结果打印输出
- 盒子案例
- 编程创建一个 Box 结构体,在其中声明三个字段表示一个立方体的长、宽和高,长宽高要从终端获取
- 声明一个方法获取立方体的体积。
- 创建一个 Box 结构体变量,打印给定尺寸的立方体的体积
- 景区门票案例
- 一个景区根据游人的年龄收取不同价格的门票,比如年龄大于 18,收费 20 元,其它情况门票免费
- 请编写 Visitor 结构体,根据年龄段决定能够购买的门票价格并输出
创建结构体变量时指定字段值
Golang 在创建结构体实例(变量)时,可以直接指定字段的值
有两种方法
工厂模式
Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题
一个结构体的声明是这样的:
因为这里的 Student 的首字母 S 是大写的,如果我们想在其它包创建 Student 的实例(比如 main 包),
引入 model 包后,就可以直接创建 Student 结构体的变量(实例)。但是问题来了,如果首字母是小写的,比如 是 type student struct {....} 就不不行了,怎么办---> 工厂模式来解决
- 使用工厂模式实现跨包创建结构体实例(变量)
- 如果 model 包的 结构体变量首字母大写,引入后,直接使用, 没有问题
- 如果 model 包的 结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决
如果 model 包的 student 的结构体的字段 Score 改成 score,我们还能正常访问吗?又应该如何解决这个问题呢
93. 面向对象编程思想 抽象
抽象介绍
我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象
面向对象编程三大特性-封装
Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
- 封装的理解和好处
- 隐藏实现细节
- 提可以对数据进行验证,保证安全合理(Age)
- 如何体现封装
- 对结构体中的属性进行封装
- 通过方法,包 实现封装
- 封装实现步骤
- 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
- 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
- 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
- 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
特别说明:在 Golang 开发中并没有特别强调封装,这点并不像 Java. 所以提醒学过 java 的朋友,
不用总是用 java 的语法特性来看待 Golang, Golang 本身对面向对象的特性做了简化的
面向对象编程三大特性-继承
一个学生考试系统的程序 extends01.go,提出代码复用的问题
- 继承基本介绍和示意图
继承可以解决代码复用,让我们的编程更加靠近人类思维。
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(如刚才的Student),在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 Student 匿名结构体即可
在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性
- 嵌套匿名结构体的基本语法
- 继承给编程带来的便利
- 代码的复用性提高了
- 代码的扩展性和维护性提高了
- 继承的深入讨论
- 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用
- 匿名结构体字段访问可以简化
- 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
- 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段
- 如果没有就去看 B 中嵌入的匿名结构体A 有没有声明 Name 字段,如果有就调用, 如果没有继续查找..如果都找不到就报错
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错
- 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
- 如果一个结构体有 int 类型的匿名字段,就不能第二个
- 如果需要有多个 int 的字段,则必须给 int 字段指定名字
多重继承
一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承
- 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分
- 为了保证代码的简洁性,建议大家尽量不使用多重继承
94. 接口 interface
按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(interface),因为在 Golang 中 多态特性主要是通过接口来体现的
interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想
- Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字
接口应用场景
注意事项和细节
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(将结构体给接口,在去调用)
- 接口中所有的方法都没有方法体,即都是没有实现的方法(接口仅写方法名)
- 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口(简单来说,如一个接口有两个方法,那么只实现了一个方法,也不叫实现了接口)
- 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型(如var stu Stu var a AInterface = stu)
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
- 一个自定义类型可以实现多个接口
- Golang 接口中不能有任何变量
- 一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法也全部实现
- interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil
- 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口
interface 练习
接口编程案例
实现接口 vs 继承
- 上述代码小结
- 当 A 结构体继承了 B 结构体,那么 A 结构就自动的继承了 B 结构体的字段和方法,并且可以直接使用
- 当 A 结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充
- 实现接口可以看作是对 继承的一种补充
- 案例2如足球运动员和大学生需要学习英语技能
- 接口和继承解决的解决的问题不同
- 继承的价值主要在于:解决代码的复用性和可维护性。
- 接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法
- 接口比继承更加灵活 Person Student BirdAble LittleMonkey
接口比继承更加灵活,继承是满足 is - a 的关系,而接口只需满足 like - a 的关系
简单来说接口是一个功能扩展,从关系来说继承弱于接口
- 接口在一定程度上实现代码解耦
95. 面向对象编程——多态
变量(实例)具有多种形态。面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态
前面的 Usb 接口案例,Usb usb ,既可以接收手机变量,又可以接收相机变量,体现了 Usb 接口 多态特性
接口体现多态的两种形式
- 多态参数
在前面的 Usb 接口案例,Usb usb ,即可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口 多态
- 多态数组
给 Usb 数组中,存放 Phone 结构体 和Camera 结构体变量
类型断言
由一个具体的需要,引出了类型断言
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
上述代码说明
- 在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型
- 如何在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic
断言实例
96. 项目 —— 家庭收支记账软件项目
项目开发流程说明
项目需求说明
- 模拟实现基于文本界面的《家庭记账软件》
- 该软件能够记录家庭的收入、支出,并能够打印收支明细表
项目的界面
项目代码实现
- 实现基本功能(先使用面向过程,后面改成面向对象)
- 功能 1: 先完成可以显示主菜单,并且可以退出
- 功能 2:完成可以显示明细和登记收入的功能
- 因为需要显示明细,我们定义一个变量 details string 来记录
- 需要定义变量来记录余额(balance)、每次收支的金额(money), 每次收支的说明(note)
- 功能 3:完成了登记支出的功能
代码实现改进
- 用户输入 4 退出时,给出提示"你确定要退出吗? y/n",必须输入正确的 y/n ,否则循环输入指令,直到输入 y 或者 n
- 当没有任何收支明细时,提示 "当前没有收支明细... 来一笔吧!”
- 在支出时,判断余额是否够,并给出相应的提示
- 将 面 向 过 程 的 代 码 修 改 成 面 向 对 象 的 方 法 , 编 写 myFamilyAccount.go , 并 使 用testMyFamilyAccount.go 去完成测试
- 记账软件的功能,封装到一个结构体中,然后调用该结构体的方法,来实现记账,显示明细。结构体的名字 FamilyAccount
- 在通过在 main 方法中,创建一个结构体 FamilyAccount 实例,实现记账即可
对项目的扩展功能的练习
- 对上面的项目完成一个转账功能
- 在使用该软件前,有一个登录功能,只有输入正确的用户名和密码才能操作
97. 项目 —— 客户系统管理
需要分析
- 模拟实现基于文本界面的《客户信息管理软件》
- 该软件能够实现对客户对象的插入、修改和删除(用切片实现),并能够打印客户明细表
项目界面
客户关系管理系统的程序框架图
分析和代码实现顺序是相反的
功能实现 显示主菜单和完成退出软件功能
- 功能说明
当用户运行程序时,可以看到主菜单,当输入 5 时,可以退出该软件
- 思路分析
编写 customerView.go ,另外可以把 customer.go 和 customerService.go 写上
- 代码实现
完成显示客户列表的功能
- 功能说明
- 思路分析
- 代码实现
项目功能实现-添加客户的功能
- 功能说明
- 思路分析
- 代码实现
项目功能实现-完成删除客户的功能
- 功能说明
- 思路分析
- 代码实现
项目功能实现-完善退出确认功能
- 功能说明
要求用户在退出时提示 " 确认是否退出(Y/N):",用户必须输入 y/n, 否则循环提示
- 思路分析
在customerView.go 编写一个退出方法判断即可
- 代码实现
项目功能实现-完成修改客户的方法
98. 文件基本操作
文件是数据源(保存数据的地方)的一种,比如大家经常使用的 word 文档,txt 文件,excel 文件...都是文件。文件最主要的作用就是保存数据,它既可以保存一张图片,也可以保持视频,声音...
输入流和输出流
os.File 封装所有文件相关操作,File 是一个结构体
打开文件和关闭文件
读文件操作应用实例
- 读取文件的内容并显示在终端(带缓冲区的方式),使用 os.Open, file.Close, bufio.NewReader(), reader.ReadString 函数和方法
- 读取文件的内容并显示在终端(使用 ioutil 一次将整个文件读入到内存中),这种方式适用于文件不大的情况。相关方法和函数(ioutil.ReadFile)
写文件操作应用实例
- 基本介绍-os.OpenFile 函数
- 创建一个新文件,写入内容 5 句 "hello, Gardon”
- 打开一个存在的文件中,将原来的内容覆盖成新的内容 10 句 "你好,尚硅谷!”
- 打开一个存在的文件,在原来的内容追加内容 'ABC! ENGLISH!’
- 打开一个存在的文件,将原来的内容读出显示在终端,并且追加 5 句"hello,北京!”
- 编程一个程序,将一个文件的内容,写入到另外一个文件。注:这两个文件已经存在了.
说明:使用 ioutil.ReadFile / ioutil.WriteFile 完成写文件的任务
判断文件是否存在
拷贝文件
统计英文、数字、空格和其他字符数量
命令行参数
我们希望能够获取到命令行输入的各种参数,该如何处理? 如图:=> 命令行参数
os.Args 是一个 string 的切片,用来存储所有的命令行参数
flag 包用来解析命令行参数
前面的方式是比较原生的方式,对解析参数不是特别的方便,特别是带有指定参数形式的命令行
如:cmd>main.exe -f c:/aaa.txt -p 200 -u root 这样的形式命令行,go 设计者给我们提供了 flag
包,可以方便的解析命令行参数,而且参数顺序可以随意
json 基本介绍
- 应用场景
序列化相当于包装,反序列相当于解析包装
- json数据格式说明
- json 数据在线解析
https://www.json.cn/ 网站可以验证一个 json 格式的数据是否正确。尤其是在我们编写比较复杂的json 格式数据时,很有用
- json序列化
json 序列化是指,将有 key-value 结构的数据类型(比如结构体、map、切片)序列化成 json 字符串的操作
- json反序列化
json 反序列化是指,将 json 字符串反序列化成对应的数据类型(比如结构体、map、切片)的操作
- 代码小结
- 在反序列化一个 json 字符串时,要确保反序列化后的数据类型和原来序列化前的数据类型一致
- 如果 json 字符串是通过程序获取到的,则不需要再对 “ 转义处理
99. 单元测试
确认一个函数或者一个模块结果是否正确
传统方法缺点分析
- 不方便, 我们需要在 main 函数中去调用,这样就需要去修改 main 函数,如果现在项目正在运行,就可能去停止项目。
- 不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在 main 函数,不利于我们管理和清晰我们思路
- 引出单元测试。-> testing 测试框架 可以很好解决问题
基本介绍 单元测试
Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试,testing 框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以解决如下问题:
- 确保每个函数是可运行,并且运行结果是正确的
- 确保写出来的代码性能是好的
- 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决, 而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定
- 快速入门
使用 Go 的单元测试,对 addUpper 和 sub 函数进行测试。
快速入门小结
- 测试用例文件名必须以 _test.go 结尾。 比如 cal_test.go , cal 不是固定的。
- 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper
- TestAddUpper(t *tesing.T)的形参类型必须是 *testing.T 【看一下手册】
- 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub
- 运行测试用例指令
- cmd>go test
- cmd>go test -v [运行正确或是错误,都输出日志]
[如果运行正确,无日志,错误时,会输出日志]
- 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序
- t.Logf 方法可以输出相应的日志
- 测试用例函数,并没有放在 main 函数中,也执行了,这就是测试用例的方便之处[原理图].
- PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败
- 测试单个文件,一定要带上被测试的原文件(后面的文件名顺序无所谓) go test -v cal_test.go cal.go
- 测试单个方法(后面写测试函数名即可) go test -v -test.run TestAddUpper
单元测试案例
100. goroutine(协程) 和 channel(管道)
goroutine-看一个需求
需求:要求统计 1-9000000000 的数字中,哪些是素数?
分析思路:
- 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。[很慢]
- 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到goroutine.【速度提高 4 倍】
goroutine基本介绍
- 进程和线程
- 程序、进程和线程的关系示意图
- 并发和并行
- 多线程程序在单核上运行,就是并发
- 多线程程序在多核上运行,就是并行
go 协程和GO主线程
- Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]
- Go 协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
goroutine快速入门
请编写一个程序,完成如下功能:
- 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 "hello,world"
- 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
- 要求主线程和 goroutine 同时执行
- 画出主线程和协程执行流程图
- 快速入门小结
- 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
- Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势
goroutine 的调度模型
- MPG 模式基本介绍
- MPG 模式运行的状态 1
- MPG 模式运行的状态 2
设置 Golang 运行的 cpu 数
介绍:为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目
channel(管道)
现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。最后显示出来。要求使用 goroutine 完成
- 分析思路
- 使用 goroutine 来完成,效率高,但是会出现并发/并行安全问题.
- 这里就提出了不同 goroutine 如何通信的问题
- 代码实现
- 使用 goroutine 来完成(看看使用 gorotine 并发完成会出现什么问题? 然后我们会去解决)
- 在运行某个程序时,如何知道是否存在资源竞争问题。 方法很简单,在编译该程序时,增加一个参数-race 即可 [示意图]
不同 goroutine 之间如何通讯
- 全局变量的互斥锁
- 使用管道 channel 来解决
使用全局变量加锁同步改进程序
- 因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map writes
- 解决方案:加入互斥锁
- 我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)
为什么需要channel
- 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
- 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
- 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于作 状态,这时也会随主线程的退出而销毁
- 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
- 上面种种分析都在呼唤一个新的通讯机制-channel
channel 的基本介绍
- channle 本质就是一个数据结构-队列【示意图】
- 数据是先进先出【FIFO : first in first out】
- 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
- channel 有类型的,一个 string 的 channel 只能存放 string 类型数据(若想存放多种类型,特殊情况下可以定义一个接口即可)
定义/声明channel
- var 变量名 chan 数据类型
- 举例: var intChan chan int (intChan 用于存放 int 数据) var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型) var perChan chan Person var perChan2 chan *Person ...
- 说明
- channel 是引用类型
- channel 必须初始化才能写入数据, 即 make 后才能使用
- 管道是有类型的,intChan 只能写入 整数 int
管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
channel 使用的注意事项
- channel 中只能存放指定的数据类型
- channle 的数据放满后,就不能再放入了
- 如果从 channel 取出数据后,可以继续放入
- 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock
channel 的遍历和关闭
- channel 的关闭
使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据
- channel 的遍历
channel 支持 for--range 的方式进行遍历,请注意两个细节
- 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
- 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
- 应用实例
管道阻塞
实例
- 需求
要求统计 1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有 goroutine和 channel 的知识后,就可以完成了 [测试数据: 80000]
- 分析 思路
传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。
使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine 去完成,完成任务时间短。
结论:使用 go 协程后,执行的速度,比普通方法提高至少 4 倍
channel 使用细节和注意事项
- channel 可以声明为只读,或者只写性质
- channel 只读和只写的最佳实践案例
- 使用 select 可以解决从管道取数据的阻塞问题
- goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
101. 反射
- 先看一个问题,反射的使用场景
之前的案例
- 使用反射机制,编写函数的适配器, 桥连接
定义两个匿名函数分别赋值给变量
在定义一个适配器函数统一处理接口,相当于每个函数传参
反射的基本介绍
- 反射可以在运行时动态获取变量的各种信息, 比如变量的类型(type),类别(kind)
- 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)
- 通过反射,可以修改变量的值,可以调用关联的方法
- 使用反射,需要 import (“reflect”)
- 示意图
反射的应用场景
反射重要的函数和概念
- 变量、interface{} 和 reflect.Value 是可以相互转换的,这点在实际开发中,会经常使用到。画出示意图
反射的快速入门
反射的注意事项和细节
- reflect.Value.Kind,获取变量的类别,返回的是一个常量
- Type 和 Kind 的区别
Type 是类型, Kind 是类别, Type 和 Kind 可能是相同的,也可能是不同的.
比如: var num int = 10 num 的 Type 是 int , Kind 也是 int
比如: var stu Student stu 的 Type 是 pkg1.Student , Kind 是 struct
- 通过反射的来修改变量, 注意当使用 SetXxx 方法来设置需要通过对应的指针类型来完成, 这样才能改变传入的变量的值, 同时需要使用到 reflect.Value.Elem()方法
- reflect.Value.Elem() 应该如何理解?
练习
反射最佳实践
- 使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值
- 使用反射的方式来获取结构体的 tag 标签, 遍历字段的值,修改字段值,调用结构体方法(要求:通过传递地址的方式完成, 在前面案例上修改即可)
- 定义了两个函数 test1 和 test2,定义一个适配器函数用作统一处理接口【了解】
- 使用反射操作任意结构体类型:【了解】
- 使用反射创建并操作结构体
102. 常量 补充
103. TCP编程
网络编程基本介绍
Golang 的主要设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端 程序必不可少也是至关重要的一部分
- TCP socket 编程,是网络编程的主流。之所以叫 Tcp socket 编程,是因为底层是基于 Tcp/ip 协议的 如QQ
- b/s 结构的 http 编程,我们使用浏览器去访问服务器时,使用的就是 http 协议,而 http 底层依旧是用 tcp socket 实现的 如京东商城 属于go web 开发范畴
协议 TCP/IP
TCP/IP(Transmission Control Protocol/Internet Protocol)的简写,中文译名为传输控制协议/因特网互联协议,又叫网络通讯协议,这个协议是 Internet 最基本的协议、Internet 国际互联网络的基础,单地说,就是由网络层的 IP 协议和传输层的 TCP 协议组成的
OSI 与 Tcp/ip 参考模型 (推荐 tcp/ip 协议 3 卷)
ip 地址
每个 internet 上的主机和路由器都有一个 ip 地址,它包括网络号和主机号,ip 地址有ipv4(32位)或者 ipv6(128 位). 可以通过 ipconfig 来查看
端口 port
这里所指的端口不是指物理意义上的端口,而是特指 TCP/IP 协议中的端口,是逻辑意义上的端口
如果把 IP 地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个门,但是一个 IP 地址的端口 可以有 65536(即:256×256)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从 0 到 65535(256×256-1)
端口(port)-分类
- 0 号是保留端口.
- 1-1024 是固定端口(程序员不要使用) 又叫有名端口,即被某些程序固定使用,一般程序员不使用. 22: SSH 远程登录协议 23: telnet 使用 21: ftp 使用 25: smtp 服务使用 80: iis 使用 7: echo 服务
- 1025-65535 是动态端口 这些端口,程序员可以使用
端口(port)-使用注意
- 在计算机(尤其是做服务器)要尽可能的少开端口
- 一个端口只能被一个程序监听
- 如果使用 netstat –an 可以查看本机有哪些端口在监听
- 可以使用 netstat –anb 来查看监听端口的 pid,在结合任务管理器关闭不安全的端口
tcp socket 编程的客户端和服务器端
下图为Golang socket 编程中客户端和服务器的网络分布
tcp socket 编程的快速入门
- 服务端处理流程
监听端口 8888
接收客户端的 tcp 链接,建立客户端和服务器端的链接.
创建 goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)
- 客户端的处理流程
建立与服务端的链接
发送请求数据[终端],接收服务器端返回的结果数据
关闭连接
104. Redis
redis基本使用
说明: Redis 安装好后,默认有 16 个数据库,初始默认使用 0 号库, 编号是 0...15
- 添加 key-val [set]
- 查看当前 redis 的 所有 key [keys *]
- 获取 key 对应的值. [get key]
- 切换 redis 数据库 [select index]
- 如何查看当前数据库的 key-val 数量 [dbsize]
- 清空当前数据库的 key-val 和清空所有数据库的 key-val [flushdb flushall]
Redis 的 Crud 操作
Redis 的五大数据类型是: String(字符串) 、Hash (哈希)、List(列表)、Set(集合)和 zset(sorted set:有序集合)
- String(字符串)
string 是 redis 最基本的类型,一个 key 对应一个 value
string 类型是二进制安全的。除普通的字符串外,也可以存放图片等数据
redis 中字符串 value 最大是 512M
set[如果存在就相当于修改,不存在就是添加]/get/del
- String(字符串)-使用细节和注意事项
setex(set with expire)键秒值
mset[同时设置一个或多个 key-value 对]
mget[同时获取多个 key-val]
- Hash (哈希,类似 golang 里的 Map)
Redis hash 是一个键值对集合。var user1 map[string]string
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象
- Hash(哈希,类似 golang 里的 Map)-CRUD
Redis 的 Hash 的 CRUD 的基本操作.
hset/hget/hgetall/hdel
- Hash-使用细节和注意事项
在给 user 设置 name 和 age 时,前面我们是一步一步设置,使用 hmset 和 hmget 可以一次性来设置多个 filed 的值和返回多个 field 的值
hlen 统计一个 hash 有几个元素
hexists key field 查看哈希表 key 中,给定域 field 是否存在
- List(列表)-介绍
列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
List 本质是个链表, List 的元素 是有序的,元素的值可以重复
List(列表)-CRUD
lpush/rpush/lrange/lpop/rpop/del/
List-使用细节和注意事项
- Set(集合)
Redis 的 Set 是 string 类型的无序集合
底层是 HashTable 数据结构, Set 也是存放很多字符串元素,字符串元素是无序的,而且元素的值不能重复
Set(集合)- CRUD
sadd key member [member ...]
smembers[取出所有值]
sismember[判断值是否是成员]
srem [删除指定值]
golang 连接 redis
安装第三方开源 Redis 库
- 使用第三方开源的 redis 库: github.com/garyburd/redigo/redis
- 在使用 Redis 前,先安装第三方 Redis 库,在 GOPATH 路径下执行安装指令: D:\goproject>go get github.com/garyburd/redigo/redis
示例
redis 连接池
说明: 通过 Golang 对 Redis 操作, 还可以通过 Redis 链接池, 流程如下:
- 事先初始化一定数量的链接,放入到链接池
- 当 Go 需要操作 Redis 时,直接从 Redis 链接池取出链接即可。
- 这样可以节省临时获取 Redis 链接的时间,从而提高效率
105. 经典项目-海量用户即时通讯系统
需求分析--> 设计阶段---> 编码实现 --> 测试阶段-->实施
- 需要分析
用户注册
用户登录
显示在线用户列表
群聊(广播)
点对点聊天
离线留言
- 界面设计
实现功能-显示客户端登录菜单
功能:可以正确的显示客户端的菜单
实现功能-完成用户登录
先完成指定用户的验证,用户 id=100, 密码 pwd=123456 可以登录,其它用户不能登录
这里需要先说明一个 Message 的组成(示意图),并发送一个 Message 的流程
- 完成客户端可以发送消息长度,服务器端可以正常收到该长度值
先确定消息 Message 的格式和结构
然后根据上图的分析完成代码
- 完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户端发送的消息(LoginMes), 判断用户的合法性,并返回相应的 LoginResMes
- 让客户端发送消息本身
- 服务器端接受到消息, 然后反序列化成对应的消息结构体.
- 服务器端根据反序列化成对应的消息, 判断是否登录用户是合法, 返回 LoginResMes
- 客户端解析返回的 LoginResMes,显示对应界面
- 这里我们需要做函数的封装
能够完成登录,并提示相应信息
程序结构的改进
- 程序结构的改进, 前面的程序虽然完成了功能,但是没有结构,系统的可读性、扩展性和维护性都不好,因此需要对程序的结构进行改进
- 先改进服务端, 先画出程序的框架图[思路],再写代码
先把分析出来的文件,创建好,然后放到相应的文件夹[包]
现在根据各个文件,完成的任务不同,将 main.go 的代码剥离到对应的文件中即可
- 修改客户端, 先画出程序的框架图[思路],再写代码
先把各个文件放到对应的文件夹[包]
代码
- 在 Redis 手动添加测试用户,并画图+说明注意. (后面通过程序注册用户)
手动直接在 redis 增加一个用户信息
如输入的用户名密码在 Redis 中存在则登录,否则退出系统,并给出相应的
提示信息:
- 用户不存在,你也可以重新注册,再登录
- 你密码不正确
实现功能-完成注册用户
完成注册功能,将用户信息录入到 Redis 中
思路分析,并完成代码
客户端
- 先把user.go放入到common/message文件夹
- common/message/message.go新增加两个消息类型
- 在客户端接收用户的输入
- 在client/process/userProcess.go增加一个Register方法,完成请求注册
- 在server/model/userDao.go增加了一个方法Register 方法
实现功能-完成登录时能返回当前在线用户
实现功能-完成登录用可以群聊
服务器可以将接收到的消息,群发给所有在线用户(发送者除外)
聊天的项目的扩展功能要求
- 实现私聊.[点对点聊天]
- 如果一个登录用户离线,就把这个人从在线列表去掉【】
- 实现离线留言,在群聊时,如果某个用户没有在线,当登录后,可以接受离线的消息
- 发送一个文件
数据结构
数据结构的介绍
- 数据结构是一门研究算法的学科,只从有了编程语言也就有了数据结构.学好数据结构可以编写出更加漂亮,更加有效率的代码。
- 要学习好数据结构就要多多考虑如何将生活中遇到的问题,用程序去实现解决.
- 程序 = 数据结构 + 算法
数据结构和算法的关系
- 算法是程序的灵魂,为什么有些网站能够在高并发,和海量吞吐情况下依然坚如磐石,大家可能会说: 网站使用了服务器群集技术、数据库读写分离和缓存技术(比如 Redis 等),那如果我再深入的问一句,这些优化技术又是怎样被那些天才的技术高手设计出来的呢?
- 大家请思考一个问题,是什么让不同的人写出的代码从功能看是一样的,但从效率上却有天壤之别,拿在公司工作的实际经历来说, 我是做服务器的,环境是 UNIX,功能是要支持上千万人同时在线,并保证数据传输的稳定, 在服务器上线前,做内测,一切 OK,可上线后,服务器就支撑不住了, 公司的 CTO 对我的代码进行优化,再次上线,坚如磐石。那一瞬间,我认识到程序是有灵魂的,就是算法。如果你不想永远都是代码工人,那就花时间来研究下算法吧!
- 数据和算法之间的关系? 数据结构是算法的基础 算法建立在数据结构之上,相对来说算法实现更容易
- 学完数据结构可以做哪些事情? 当实际需求遇到问题时,相对容易将算法写出来,简单来说是有工具
稀疏 sparsearray 数组
- 编写的五子棋程序中,有存盘退出和续上盘的功能
- 分析按照原始的方式来的二维数组的问题 因为该二维数组的很多值是默认值 0, 因此记录了很多没有意义的数据
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组
稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
- 思想:把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
稀疏的本质是压缩,压缩本质将一些有用的数据记录,默认的数据记录一条
应用实例
- 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
- 把稀疏数组存盘,并且可以从新恢复原来的二维数组数
- 整体思路分析
- 代码实现
队列 queue
- 队列是一个有序列表,可以用数组或是链表来实现。
- 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
- 示意图:(使用数组模拟队列示意图)
数组模拟队列
先完成一个非环形的队列(数组来实现)
对上面代码的小结和说明:
1) 上面代码实现了基本队列结构,但是没有有效的利用数组空间
2) 请思考,如何使用数组 实现一个环形的队列
数组模拟环形队列
分析思路:
- 什么时候表示队列满 (tail + 1) % maxSize = head
- tail == head 表示空
- 初始化时, tail = 0 head = 0
- 怎么统计该队列有多少个元素 (tail + maxSize - head ) % maxSize
链表
链表是有序的列表,但是它在内存中是存储如下
单链表的示意图
说明:一般来说,为了比较好的对单链表进行增删改查的操作,我们都会给他设置一个头结点, 头
结点的作用主要是用来标识链表头, 本身这个结点不存放数据
单链表的应用实例
案例的说明:
使用带 head 头的单向链表实现 –水浒英雄排行榜管理
完成对英雄人物的增删改查操作, 注: 删除和修改,查找可以考虑学员独立完成
第一种方法在添加英雄时,直接添加到链表的尾部
双链表应用实例
环形单向链表介绍
排序
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序(最快)
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,经过和其他元素重整,再依原则交换位置后达到排序的目的
选择排序思想:(取最小值)
选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从 R[0]~R[n-1]中选取最小值,与 R[0]交换,第二次从 R[1]~R[n-1]中选取最小值,与 R[1]交换,第三次从 R[2]~R[n-1]中选取最小值,与 R[2]交换,…,第 i 次从 R[i-1]~R[n-1]中选取最小值,与 R[i-1]交换,…, 第 n-1 次从R[n-2]~R[n-1]中选取最小值,与 R[n-2]交换,总共通过 n-1 次,得到一个按排序码从小到大排列的有序序列
插入排序法
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的
插入排序法思想:
插入排序(Insertion Sorting)的基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表
快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
三种排序方法的速度的分析
栈
有些程序员也把栈称为堆栈, 即栈和堆栈是同一个概念
- 栈的英文为(stack)
- 栈是一个先入后出(FILO-First In Last Out)的有序列表。
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)
- 根据堆栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中
- 表达式的转换与求值
- 二叉树的遍历
- 图形的深度优先(depth 一 first)搜索法
栈实现综合计算器
递归
递归就是函数/方法自己调用自己,每次调用时传入不同的变量.第归有助于编程者解决复杂的问题,同时可以让代码变得简洁
- 打印问题
- 阶乘问题
- 示意图
- 递归用于解决什么样的问题
各种数学问题如: 8 皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
将用栈解决的问题-->第归代码比较简洁
- 递归需要遵守的重要原则
- 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
- 函数的局部变量是独立的,不会相互影响, 如果希望各个函数栈使用同一个数据,使用引用传递
- 递归必须向退出递归的条件逼近【程序员自己必须分析】,否则就是无限递归,死龟了:)
- 当一个函数执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁
哈希表(散列)
有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址..),当输入该员工的 id 时,要求查找到该员工的 所有信息.
要求: 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
哈希表的基本介绍
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表
使用 hashtable 来实现一个雇员的管理系统[增删改查]
- 要求:
- 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
- 添加时,保证按照雇员的 id 从低到高插入
- 思路分析
- 使用链表来实现哈希表, 该链表不带表头 [即: 链表的第一个结点就存放雇员信息]
- 思路分析并画出示意图
Loading...
Last update: 2022-09-05