面向对象思想
面向对象简介
编程思想
与编程语言无关。
C语言、Go中的 结构体 就是后来面向对象编程语言中的类。
面向对象编程:高内聚,低耦合。
特性
- 继承 —— 匿名字段(实名字段)
- 封装 —— 方法
- 多态 —— 接口(interface)
Go 语言是典型的面向对象编程语言。
通过程序描述对象
创建类(指定类属性)
- 类属性:静态。---- 名词、形容词
// go语言中的类就是结构体--类型(int/bool/byte/string) type Student struct { // 定义Student类 name string // 类属性(域)、成员属性、成员变量 age int marry bool addr string // } // 按照类创建对应的类对象(实例、对象) var stu Student = Student{"亚瑟", 36, false, "峡谷"}
绑定类方法
- 方法:动态。--- 动词。
func (s Student) study() { fmt.Println("I 'm a student, good good study, day day up") } // 使用学生类对象,调用学生的方法 stu.study()
匿名字段
实名字段
- 在继承中。子类包含父类属性。字段名 和 字段类型都有。
type Person struct { // 父类 name string age int sex string } type Student struct { // 子类 per Person // 实名字段。 score int }
匿名字段
- 在继承中。子类包含父类属性。只有字段类型。
type Person struct { // 父类 name string age int sex string } type Student struct { // 子类 Person // 匿名名字段。 score int }
子类对象在访问父类成员时,实名字段必须,通过字段名访问父类成员。
stu.per.name stu.per.age // 实名 stu.Person.name //匿名字段完整写法 stu.name stu.age //匿名,Go优化后简便写法 -- 推荐
同名字段
- 父类属性和子类属性(字段名、字段类型)相同
- 子类访问同名字段时,采用 “就近原则” 。使用子类属性。(前提:匿名字段。)
- 子类、父类同名字段,占用的内存不同。
当程序中,多次出现 “同名字段” 时, 采用 实名字段方法解决。尽量避免。
指针匿名字段
在子类继承父类属性时,包含 指针类型的 父类属性。
// 创建父类 type Person4 struct { name string // 同名字段 age int marry bool } // 创建子类,从父类继承 type Student4 struct { *Person4 // 使用指针匿名字段,从父类继承 score int }
子类访问指针匿名字段:
(*stu.Person4).name = "三傻" // 方法1:完整打印父类成员属性。 (stu.Person4).name = "三傻" // 方法2:Go优化后写法 stu.name = "三傻" // 方法3:直接访问父类属性。
指针匿名字段,在访问时,应先检查是否为 nil 指针。再使用。
// 给指针匿名字段初始化 if stu.Person4 == nil { stu.Person4 = new(Person4) }
继承关系
程序编译的过程
预处理 --- 代码的替换
编译 --- 词法分析、语法分析、开辟内存(data(初始化的全局变量)/rodata/bss)、转换为汇编
汇编 --- 转换为 二进制
链接 --- 数据段合并、地址回填。
结论:
不允许 父子类之间,使用 普通字段 相互继承。(指针字段可以)
类中不可以使用 普通字段 自己嵌套自己 (指针字段可以)
type LinkNode struct { Data int // 8 //Next LinkNode // ??? Next *LinkNode // 8 }
多重继承
- C —— B —— A
// 父类的父类
type Human6 struct {
name string
age int
}
// 父类
type Person6 struct {
Human6
sex string
}
// 子类
type Student6 struct {
Person6
score int
}
// 创建子类对象
stu := Student6{Person6{Human6{"林黛玉", 21}, "男"}, 100}
// 访问对象成员
stu.Person6.Human6.name = "贾宝玉" // 完整写法
stu.Person6.name = "贾宝玉"
stu.Human6.name = "贾宝玉"
stu.name = "贾宝玉"
C —— B
C —— A
// 子类的父类
type Human7 struct {
name string
age int
}
// 子类的另一个父类
type Person7 struct {
addr string
sex string
}
// 子类
type Student7 struct {
Human7
Person7
score int
}
func main() {
// 创建子类对象
var stu Student7
// 访问对象成员,赋值
stu.name = "林冲" // stu.Human7.name = "xxx"
stu.age = 31
stu.sex = "男"
stu.addr = "汴梁"
fmt.Println(stu)
}
- 应该在编程过程中,尽量避免多重继承。防止过高耦合,防止大量同名字段产生。
方法的定义和使用
// 类方法的定义语法
func (绑定对象/方法接受者) 类方法名(形参列表)返回值列表 {
方法体(编码)
return 返回值列表
}
- 使用方法
// 1. 创建类对象
var stu Student
// 2. 使用类对象,调用类方法
stu.类方法(实参列表)
// 方法调用时,有两处赋值
1)实参,赋值给形参。
2)方法调用对象,给“绑定对象”赋值。
// 创建类
type Student8 struct {
name string
age int
}
// add 被指定为 Student8 类的方法。 只有Student8类对象才能调用该方法。
func (s Student8) add(a int, b int) {
sum := a + b
fmt.Println("sum =", sum)
}
func main() {
//add(10, 20) // 函数调用。
// 创建类对象
var stu Student8 // stu :=
// 使用类对象,调用类方法
stu.add(10, 20)
}
结构体类型方法
- 子类对象可以继承父类的属性同时也可以继承父类方法。
// 子类的另一个父类
type Person9 struct {
name string
addr string
sex string
}
// 子类
type Student9 struct {
Person9 // 继承!!!—— 子类有父类的属性。同时也能使用父类的方法。
score int
}
// 绑定 父类方法
func (per Person9) personInfo() {
fmt.Printf("大家好,我叫%s,我是%s生,我来自%s\n", per.name, per.sex, per.addr)
}
func main() {
//var per = Person9{"武大郎", "阳谷县", "男"}
//per.personInfo()
stu := Student9{Person9{"武二郎", "阳谷县", "女"}, 98}
stu.personInfo()
}
基础类型方法
// 创建基础数据类
type INT int // 基础数据类型名 --- 大写 --- 类
// 绑定方法
func (a INT) add(b INT) INT {
return a + b
}
func main() {
//定义类对象
//var m int = 10 // 不是 INT 类对象
//var n int = 20
var m INT = 10 // 是 INT 类对象
var n INT = 20
// 调用类方法
ret := m.add(n)
fmt.Println("ret =", ret)
}
结构体指针方法
- 方法的调用者,是一个普通结构体对象
- 方法的调用者, 是结构体指针变量。
【总结】:
结构变量,作为方法绑定对象时,只能做“读”,无法修改对象的属性。
结构体指针变量,作为方法的绑定对象时,既可以“读”, 又可以“写”。可以 修改对象的属性。
并且,Go语言优化后的使用方法,与普通结构体变量方法一致。
// 子类的另一个父类 type Person11 struct { name string addr string sex string } // 子类 type Student11 struct { Person11 score int } func (per *Person11) change() { // 修改 name 属性 //(*per).name = "杨二郎" // 真正使用地址访问属性 per.name="杨小妹" // Go优化的 解引用。 fmt.Println(*per) } func main() { stu := Student11{Person11{"武二郎", "阳谷县", "女"}, 98} // 借助change函数,修改 name //(&stu).change() stu.change() // Go语言优化后的简便写法。 // 打印 fmt.Println(stu) }
方法重写
- 子类对象绑定的 方法名 与父类的方法名一致。发生方法重写。
- 重写特性:
- 默认子类对象调用 子类方法。
- 子类对象使用父类的类名索引父类方法。
// 公共类
type Person13 struct {
name string
age int
sex string
}
// 程序类,从父类继承
type Developer13 struct {
Person13
workyears int
}
// 绑定父类方法
func (per *Person13) SayHello13() {
fmt.Printf("大家好,我叫%s,我今年%d岁,我是%s生,", per.name, per.age, per.sex)
}
// 绑定子类方法 -- 程序员 -- 方法重写。
func (dev *Developer13) SayHello13(a int) int {
fmt.Println("我是子类:", dev, "a =", a)
return a+10
}
func main() {
//创建子类对象 —— 程序员
dev := Developer13{Person13{"图灵", 57, "男"}, 30}
// 默认方法重写后,子类对象调用子类方法。
ret := dev.SayHello13(9527)
fmt.Println("ret=", ret)
// 方法重写后,子类可以 使用 父类类名索引父类方法。
dev.Person13.SayHello13()
}
方法值和方法表达式
方法值
- 就是内存中 text (代码区) (只读)上的一块地址值。与函数值类似。
- 可以使用指针保存,方法值。
方法表达式
定义 函数指针变量
var p func(int) int // 类型受函数原型影响。
使用函数名、方法名 初始化 p
p = stu.test // 方法名 p = add // 函数名
借助 p 调用函数。因此函数、方法原型
ret = p(10) // 传递实参。
面向对象计算器实现
1. 继承、封装
// 继承和封装实现加、减法
// 父类
type Operate struct {
n1 int
n2 int
}
// 创建 加法子类,继承于父类
type AddOpt struct {
Operate
}
// 创建 加法子类,继承于父类
type SubOpt struct {
Operate
}
// 绑定加法方法
func (a *AddOpt) Result() int {
return a.n1 + a.n2
}
// 绑定加法方法
func (s *SubOpt) Result() int {
return s.n1 - s.n2
}
func main() {
/* // 创建加法对象
add := AddOpt{Operate{10, 20}}
// 加法对象,调用方法
ret := add.Result()
fmt.Println("ret =", ret)*/
// 创建减法对象
sub := SubOpt{Operate{10, 20}}
// 减法对象,调用方法
ret := sub.Result()
fmt.Println("ret =", ret)
}
2. 接口
// 添加接口,使用接口调用方法。
// 创建接口
type Calcer interface {
Result() int
}
// 父类
type Operate3 struct {
n1 int
n2 int
}
// 创建 加法子类,继承于父类
type AddOpt3 struct {
Operate3
}
// 创建 加法子类,继承于父类
type SubOpt3 struct {
Operate3
}
// 绑定加法方法
func (a *AddOpt3) Result() int {
return a.n1 + a.n2
}
// 绑定加法方法
func (s *SubOpt3) Result() int {
return s.n1 - s.n2
}
func main() {
// 创建减法对象
sub := SubOpt3{Operate3{10, 20}}
// 创建接口变量
var cal Calcer
// 使用类对象给接口变量赋值
cal = &sub
// 使用接口变量调用方法
ret := cal.Result()
fmt.Println("ret =", ret)
}
3. 多态
//
// 创建接口
type Calcer5 interface {
Result5() int
}
// 父类
type Operate5 struct {
n1 int
n2 int
}
// 创建 加法子类,继承于父类
type AddOpt5 struct {
Operate5
}
// 创建 加法子类,继承于父类
type MulOpt5 struct {
Operate5
}
// 创建 加法子类,继承于父类
type SubOpt5 struct {
Operate5
}
// 绑定加法方法
func (a *AddOpt5) Result5() int {
return a.n1 + a.n2
}
// 绑定加法方法
func (s *SubOpt5) Result5() int {
return s.n1 - s.n2
}
// 绑定乘法方法
func (s *MulOpt5) Result5() int {
return s.n1 * s.n2
}
// 添加多态, 定义函数,指定接口做参数
func test(res Calcer5) int {
return res.Result5()
}
func main() {
add := AddOpt5{Operate5{10, 20}}
sub := SubOpt5{Operate5{10, 20}}
mul := MulOpt5{Operate5{10, 20}}
ret := test(&sub)
fmt.Println("ret =", ret)
ret = test(&add)
fmt.Println("ret =", ret)
ret = test(&mul)
fmt.Println("ret =", ret)
}
4. 工厂模式
// 提供满足条件的数据,不关心过程,直接获取结果
// 创建接口
type Calcer7 interface {
Result7() int
}
// 父类
type Operate7 struct {
n1 int
n2 int
}
// 创建 加法子类,继承于父类
type AddOpt7 struct {
Operate7
}
// 创建 加法子类,继承于父类
type MulOpt7 struct {
Operate7
}
// 创建 加法子类,继承于父类
type SubOpt7 struct {
Operate7
}
// 绑定加法方法
func (a *AddOpt7) Result7() int {
return a.n1 + a.n2
}
// 绑定加法方法
func (s *SubOpt7) Result7() int {
return s.n1 - s.n2
}
// 绑定乘法方法
func (s *MulOpt7) Result7() int {
return s.n1 * s.n2
}
// 添加多态, 定义函数,指定接口做参数
func test7(res Calcer7) int {
return res.Result7()
}
// 创建工厂类
type Factory struct {
// 没有类属性。只用来绑定方法。
}
// 绑定工厂类方法
func (f *Factory)CreateFactory(a int, b int, ch byte) {
// 分开处理各种运算式
switch ch {
case '+':
add := AddOpt7{Operate7{a, b}}
fmt.Printf("%d %c %d = %d\n",a, ch, b, test7(&add))
case '-':
sub := SubOpt7{Operate7{a, b}}
fmt.Printf("%d %c %d = %d\n",a, ch, b, test7(&sub))
case '*':
mul := MulOpt7{Operate7{a, b}}
fmt.Printf("%d %c %d = %d\n",a, ch, b, test7(&mul))
default:
fmt.Println("暂不支持此种运算")
}
}
func main() {
// 创建工厂类对象
var f Factory
f.CreateFactory(10, 20, '+')
f.CreateFactory(10, 20, '-')
f.CreateFactory(10, 20, '*')
f.CreateFactory(10, 20, '/')
}
- 使用 继承、封装、接口、多态、工厂模式。—— 计算器。
什么是接口
接口:规则和标准。不参加具体的实现。只包含方法原型。
函数:
函数定义:func关键字 函数名(形参列表) 返回值列表 {
函数体
return 返回值列表
}
函数调用:函数名(实参列表)
函数原型:函数名(形参列表) 返回值列表
接口的定义和使用
定义接口:
type 接口名 interface {
方法名(形参列表) 返回值列表 // 方法原型
}
建议,接口名以er结尾。
使用接口
定义接口
type Humaner interface { PrintInfo() }
按接口的定义语法,实现方法(创建类,绑定类方法)
type Person struct { } func (per *Person) PrintInfo() { fmt.Printf("大家好,我叫%s,我今年%d岁,我%s结婚,来自于%s\n", per.name, per.age, per.marry, per.addr) }
创建接口变量
var h Humaner
使用类对象给接口变量赋值
h = &per
使用接口变量调用方法。
h.PrintInfo()
多态
Go语言实现多态
- Go语言实现多态,封装一个普通函数,将接口设计为函数的参数。
- 函数内,使用参数(接口)。调用方法。
多态使用步骤
- 定义接口
- 创建类,绑定方法,实现接口
- 创建函数,指定接口类型为参数
- 函数内,使用接口,调用方法
// 定义接口
type Humaner4 interface {
Info()
}
// 创建Teacher类
type Teacher4 struct {
name string
age int
}
// 创建 Doctor 类
type Doctor4 struct {
name string
age int
knife string
}
// 给类按接口绑定方法 。
func (d *Doctor4) Info() {
fmt.Println("name=", d.name, "age=", d.age, "knife=", d.knife)
}
// 给类按接口绑定方法 —— 绑定给了Teacher4 , 只有Teacher4 对象能调用。
func (t *Teacher4) Info() {
fmt.Println("name=", t.name, "age=", t.age)
}
// 添加多态。指定接口作为参数 —— 没有绑定给任何对象,可以任意调用。
func Info(human Humaner4) {
// 使用接口变量调用方法
human.Info()
}
func main() {
// 创建类对象
teacher := Teacher4{"袁腾飞", 36}
Info(&teacher) // 使用多态
// 创建 doctor 实例
doc := Doctor4{"华佗", 97, "金丝大环刀"}
Info(&doc)
}
小结
- 编码未来!
- 前期编写的程序,按接口实现多态,能适应未来新对象、方法使用。
接口继承和转换 [子集-超集] (了解)
接口可以使用 继承来得到父类接口的 方法。 语法参照 类继承语法。
// 创建接口 --- 子集 (少) type Humaner8 interface { SayHello() } // 创建 Developer8 继承 Humaner8 --- 超集(多) type Developer8 interface { Humaner8 SayHelloWorld() }
转换:
- 子集 不可以赋值给 超集。
- 超集可以赋值给 子集。
空接口
- var i interface{}
- 默认值:
- 默认类型:
- 作用:接收任意类型的数据。
- 特性:
- 接收何种数据,就变为何种类型。
- 只能存储,不能用来运算。如果想使用运算,需要 “类型断言”
类型断言
应用场景:
- 只能应用于 “空接口” ( interface{} )类型。
语法:
- value, ok := 变量名.(类型) ---- 判断变量是否为 () 内的类型
- 是!ok 返回 true, value 保存变量值。
- 不是!ok 返回 false,value 保存变量对应实际类型的 零值。
value, ok := i1.(bool) if ok == true { fmt.Println("value=", value, "ok=", ok) } // 简化成: if val, isType := i1.(bool); isType { fmt.Println("value=", value, "ok=", ok) } // 可以使用 switch ... case switch v.(type) { // 取出 interface 变量的 类型 case int : fmt.Println(v, "是 int 类型数据") // 默认,相当于 每个 case分支中,都自动添加了 break。 go语言中,不存在case穿透 }
- value, ok := 变量名.(类型) ---- 判断变量是否为 () 内的类型
异常处理
异常种类
- 编辑错误:语法错误!GoLand 工具可以查找。 无法编译程序,不能生成 .exe
- 编译错误:第三方函数库使用时。可以进行编译,无法生成 .exe
- 运行时错误:既可以编译,也可以生成 .exe。只出现在程运行期间。程序不能正常终止。
==运行时异常==error -- 未雨绸缪
==封装异常==:
func test11(a, b int) (ret int, err error) { // 判断除数是否为0 if b == 0 { // 产生错误信息。 err = errors.New("除数不允许为0!") return } ret = a / b return }
处理异常:
ret, err := test11(10, 0) if err != nil { fmt.Println("test11 err:", err) return }
Panic异常
- 也是运行时!比 error 更为严重!不可逆!—— 只能在编码初期提早预见。
- 异常产生
- 自动产生:由系统判断,产生。
- 手动产生:立即终止程序。
- 解决方法,需要使用 defer、recover。
defer关键字
- 用来对指令(函数、表达式)进行延迟调用。延迟到当前指令对应的函数运行结束, 栈帧释放前。
recover 错误拦截
拦截 panic 异常。单独无法工作。必须依赖 defer
固定用法:
// 函数起始位置,注册 recover 拦截监听。在函数调用结束时,返回监听结果。 defer func() { fmt.Println(recover()) }() panic异常。
Channel
channel的定义
- 定义语法:
- var ch = make(chan 通道中传递的数据类型, 容量大小)
- 例:ch := make(chan int)
- 例:ch := make(chan string, 0)
- var ch = make(chan 通道中传递的数据类型, 容量大小)
- 读写:
- 读channel :
- <-ch 读到数据,丢弃
- num := <-ch 读到数据,存入 num中
- 写 channel:
- ch <- data data类型严格与 定义的语法一致
- 读channel :
- 特性:
- 通道中的数据只能单向流动。一端读端、另外必须写端。
- 通道中的数据只能一次读取,不能重复读。先进先出。
- 读端 和 写端在不同的 goroutine 之间。
- 读端读,写端不在线,读端阻塞。写端写,读端不在线,写端阻塞。
- 系统 3 个特殊文件:系统打开、系统关闭。
- stdin:标准输入文件(标准输入缓冲区) —— 键盘 —— 0
- stdout:标准输出文件(标准输出缓冲区) —— 屏幕 —— 1
- stderr:标准错误文件(标准错误缓冲区) —— 屏幕 —— 2
go程间通信
- 多个go程间,如果有多份共享资源时,需要分别同步。
channel 的分类
无缓冲channel
- 要求 读端和写端必须同时在线。对端不在线,本端阻塞。
有缓冲channel
- 定义语法: ch := make(chan 数据类型 ,容量) 容量是有实际数据。 != 0
- 读端,不在线,写端可以将数据直接写入缓冲区(不阻塞)。直到缓冲区被写满,依然没有读端,写端阻塞。
同步通信、异步通信
- 同步通信:—— 无缓冲 channel
- 一个调用发出,如果没有得到结果,那么该调用不返回。 —— 阻塞
- 相当于 打电话。双方必须同时在线。
- 异步通信:—— 有缓冲 channel
- 一个调用发出,不等待结果,直接返回。 —— 不阻塞。
- 相当于 发短信。双方不需同时在线。一端发送完,立即返回。
关闭channel
当写端,写完数据后,使用 close(ch) 关闭 channel
读端,可以判断channel是否可读:
for { if num, isOK := <-ch; isOk { // 不能写成 ch fmt.Println("num=", num) // 读到ch中数据 } else { // 写端关闭 break // 结束读操作。 } }
如果写端没关闭:
- isOk == true, 数据被读至 num 中
如果写端关闭:
- isOK == false,num为数据类型 零值(默认值)
简便写法:
for num := range ch { // 不能写成 <-ch fmt.Println("num=", num) }
关闭channel的特性:
- 如果写端没有关闭,暂停写入,读端阻塞等待。
- 如果写端已经关闭,不能写入数据。再写入,报错:panic:send on closed channel
- 如果写端已经关闭,读端依然能读取,读到的是数据类型零值(默认值)。
单向channel
定义语法:
双向channel:
- 定义语法:var ch chan int
单向读channel:
- 定义语法:var chr <-chan int
- chr = ch 双向channel 给单向读 channel 赋值。
- 只能做 读操作。如果写,报错:
- invalid operation: chr <- 888 (send to receive-only type <-chan int)
单向写channel:
- 定义语法:var chw chan<- int
- chw = ch 双向channel 给单向写 channel 赋值。
- 只能做 写操作,如果读,报错:
- invalid operation: <-chw (receive from send-only type chan<- int)
单向channel *不能 *给双向channel赋值。
单向channel的使用:
- 主要应用于函数传参。单向 channel 可以在语法层面限定 channel 使用的方式。
- 单向读:不能进行 写操作。
- 单向写:不能进行 读操作。
生产者消费者模型
- 生产者模块:产生数据。放置到公共区
- 消费者模块:从公共区消费数据。
- 公共区(缓冲区):
- 解耦:生产者和消费者,耦合度降低
- 并发:生产者和消费者处理数据的速度可以不一致。不影响各自并发。
- 缓存:生产者可以利用公共区的缓冲能力,暂存数据,提高生产效率。
定时器:Timer
创建定时器:
func NewTimer(d Duration) *Timer timer := time.NewTimer(time.Second * 3)
type Timer struct {
C <-**chan** Time // 当定时时长到达时,系统会写入当前时间 r runtimeTimer
}
读 Timer 的C 成员。定时时间没到,会阻塞。定时时长到达时,系统会写入当前时间,解除阻塞
t := <-timer.C
time.After()
合并 上述 1. 2. 为一步操作
fmt.Println(" ",time.Now())
t := <-time.After(time.Second * 3)
fmt.Println("t:", t)
Select关键字
select 基本使用
作用:监听channel上的数据流动
语法:
for { select { case <- chan1: // 如果chan1成功读到数据,则进行该case处理语句 case chan2 <- 1: // 如果成功向chan2写入数据,则进行该case处理语句 default: // 如果上面都没有成功,则进入default处理流程 } }
特性:
- 每一个case分支,都必须一个 IO操作(channel r/w事件)。
- 通常将 select 置于 for 循环中。
- 一个case监听的 channel 不满足监听条件。当前case分支阻塞。
- 当所有case分支都不满足监听条件时,select如果包含default分支,走default;select等待case。
- 当监听的多个case分支中,同时有多个case满足,随机选择任意一个执行。
- 为防止忙轮询,可以适当选择省略 default
break关键字不能应用于结束整个 select。 只能跳出一个case分支。
借助select实现超时
作用:借助 select 和 time.After() 实现超时处理
步骤:
创建 select , 启动监听 channel
for { select { case num := <-ch: fmt.Println("num =", num) case <-time.After(time.Second * 3): //1.创建 Timer, 2.读 C 成员 fmt.Println("子go程读到系统时间, 定时满 3 秒") quit <- false case case case } }
监听超时定时器:
case <-time.After(time.Second * 3)
当select监听的其他case分支满足时,time.After所在的case分支,会被重置成初始定时时长。
直到在select 监听期间,没有任何case满足监听条件。time.After 才能定时满。
死锁
- 不是锁的一种。是错误使用锁的现象。
常见死锁
- 单个go程使用同一个channel自己读、自己写。
- 多个go程使用 channel通信,go程创建之前,对channel读、写造成死锁。
- 多个go程使用多个channel 通信,相互依赖造成死锁。
- 多个go程使用 锁(读写锁、互斥锁)和 channel 通信。
互斥锁mutex
作用:保护公共区,防止多个控制流共同访问共享资源时造成数据混乱。
建议锁。不具备强制性。
使用注意事项:
- 访问公共区之前,加锁。访问结束立即解锁。
- 粒度越小越好
使用方法:
创建互斥量
var mutex sync.Mutex
加锁
mutex.Lock()
...... 访问公共区
解锁
mutex.Unlock()
读写锁RWMutex
特性
- 读共享,写独占
- 写锁优先级高
- 锁只有一把,具备两种属性(r/w)。加锁时根据使用需求,选择锁属性。
- 建议锁。不具备强制性。
使用注意事项:
- 访问公共区之前,加锁。访问结束立即解锁。
- 粒度越小越好
使用方法:
创建读写锁
var rwmutex sync.RWMutex
加读锁
rwmutex.RLock()
加写锁rwmutex.Lock()
...... 访问公共区
解读锁
rwmutex.RUnlock()
解写锁rwmutex.Unlock()
- 建议:不要将锁 和 channel 混合 使用。 —— 条件变量。
条件变量
条件变量,不是锁的一种!要结合锁使用。
模型分析
条件变量使用的函数
wait 方法
func (c *Cond) Wait()
阻塞等待条件变量满足
释放已经掌握的互斥锁。(解锁)。要求,在调用wait之前,先加锁。c.L.Lock()
。。。。 阻塞等待条件变量满足
解除阻塞,重新加锁。
signal方法
func (c *Cond) Signal()
- 唤醒阻塞在条件变量上的 go程。
Broadcast方法
func (c *Cond) Broadcast()
- 唤醒阻塞在条件变量上的 所有 go程。
条件变量实现步骤:-- 生产者
创建条件变量 var cond sync.Cond
初始化条件变量的成员 L ,指定锁(互斥、读写) cond.L = new(sync.Mutex)
加锁 cond.L.Lock()
判断条件变量满足, 调用 wait
for len(ch) == cap(ch) { // 此处必须使用 for 循环判断 wait是否满足条件。 cond.Wait() // 1 2 3 }
产生数据,写入公共区
唤醒阻塞在条件变量上的 对端。cond.Signal()
解锁 cond.L.UnLock()
waitGroup
- 主go程等待子go程结束运行。
实现步骤
- 创建 waitGroup对象。 var wg sync.WaitGroup
- 添加 主go程等待的子go程个数。 wg.Add(数量)
- 在各个子go程结束时,调用 wg.Done()。 将主go等待的数量-1. defer wg.Done
- 如果是实名子go程的话, 传地址。
- 在主go程中等待。 wg.wait()
func printer10(str string) {
for _, ch := range str {
fmt.Printf("%c", ch)
time.Sleep(time.Millisecond * 222)
}
}
func person10(wg *sync.WaitGroup) {
// 在zigo程内部, 运行结束时, Done, 将主go等地的数量 -1
defer wg.Done()
printer10("hello")
}
func person11(wg *sync.WaitGroup) {
defer wg.Done()
printer10("world")
}
func main() {
// 创建 waitGroup对象
var wg sync.WaitGroup
// 添加要等待的子go程个数
wg.Add(2)
go person10(&wg) // 实名go程传参,必须传&
go person11(&wg)
// 主go程调用 wait等待
wg.Wait()
*/
/* for {
;
}*//*
}*/
网络概述
协议
- 一组通信规则。要求数据通信双方,在通信过程中,严格遵守。
典型协议
应用层:Http、ftp
传输层:TCP、UDP
网络层:IP、ICMP、IGMP
链路层:ARP、RARP
网络分层模型
- OSI七层模型:
- 物、数、网、传、会、表、应
- TCP/IP 4层模型:
- 链、网、传、应
网络数据通信过程:
数据封装:数据 —— 应用层(非必须) —— 传输层 —— 网络层 —— 链路层 —— 以太网
数据解封装:以太网 —— 链路层 —— 网络层 —— 传输层 —— 应用层(非必须) —— 数据
各层功能:
链路层:
- 从 设备 到 设备
- 源mac —— 目标mac(不需要用户指定)
- ARP协议: 借助IP,获取 MAC地址。 RARP: 借助 MAC地址,找到 IP地址。
网络层:
- 从 节点 到 节点(主机)
- 源IP —— 目标IP(需要用户指定)
- IP协议:通过IP地址,在网络环境中唯一标识一台主机。
- 大小:4字节。 每一个字节取值范围 0-255
传输层:
- 从 进程 到 进程
- 源port —— 目标port (需要用户指定)
- TCP/UDP:通过端口号(port)在一台主机上 唯一标识一个 进程。
- IP + port 在网络环境中,唯一标识一个应用(进程)—— socket
- 大小:2字节。 2^16 = 65536 (0-65535)
- 应用层:
- 从数据封装 到 数据解封
- 源应用协议 —— 目标应用协议。
- 应用层协议可选。非必须。
socket编程
网络应用设计模型
C/S | B/S | |
---|---|---|
优点 | 缓存数据量大,协议选择灵活。 | 用户安全性高。开发工作量小。跨平台性较好 |
缺点 | 用户安全性低。开发工作量大。跨平台性差。 | 不能缓存大量数据。协议选择受限。功能受限。 |
应用场景 | 大型应用、游戏(提前缓存数据) | 跨平台性要求较高。 |
socket
在一次数据通信过程中 ,socket 必须 成对 出现。
socket :双向 全双工通信。(socket 即可以读又可以写。)
- 双向 半双工通信。channel。
- 单工通信:遥控器。
socket 内核实现:
TCP-CS-Server
实现流程
创建监听器 listener
启动监听,接收客户端连接请求 listener.Accept() --- conn
for {
conn.read 客户端发送的数据
使用、处理数据
回发 数据给客户端 write
关闭连接 Close
}
客户端使用 nc (netcat)测试:
- 语法:nc 服务器IP地址 服务器port
==使用的方法==
Listen函数 —— 创建监听器
func Listen(net, laddr string) (Listener, error)
参数1:协议类型:tcp、udp (必须小写)
参数2:服务器端的 IP: port (192.168.31.11:8000)
返回值:成功创建的监听器
type Listener interface { // Addr返回该接口的网络地址 Addr() Addr // Accept等待并返回下一个连接到该接口的连接 Accept() (c Conn, err error) // Close关闭该接口,并使任何阻塞的Accept操作都会不再阻塞并返回错误。 Close() error }
Accept 方法 —— 阻塞等待客户端连接
func (listener *Listener) Accept() (c Conn, err error)
返回值:成功与客户端建立的 连接conn(socket)
type Conn interface { // Read从连接中读取数据 // Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 Read(b []byte) (n int, err error) // Write从连接中写入数据 // Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 Write(b []byte) (n int, err error) // Close方法关闭该连接 // 并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误 Close() error // 返回本地网络地址 LocalAddr() Addr // 返回远端网络地址 RemoteAddr() Addr 。。。 }
TCP-CS-Client
实现流程:
创建与服务器的连接 net.Dial() -- conn(socket)
for {
发送数据给服务器 conn.write
接收服务器回发的 数据 conn.read
关闭连接。
}
使用的方法
Dial方法
func Dial(network, address string) (Conn, error)
参数1:使用的协议:udp、tcp
参数2:服务器的 IP:port (127.0.0.1:8000)
返回:conn 用于与服务器进行数据通信的 socket
type Conn interface { // Read从连接中读取数据 // Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 Read(b []byte) (n int, err error) // Write从连接中写入数据 // Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 Write(b []byte) (n int, err error) // Close方法关闭该连接 // 并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误 Close() error // 返回本地网络地址 LocalAddr() Addr // 返回远端网络地址 RemoteAddr() Addr }
单客户端和单服务器通信
服务器:包含 2 个socket。
一个用于监听客户端连接事件 Listenter。
另一个用户 与 客户端进行数据通信。conn
客户端:包含 1 个 socket
- 用来与服务器进行数据通信。
并发服务器:
单服务器 多客户端(1:N)
思路分析
服务器包含: 主go程、子go程们。
- 主go程:负责监听 客户端的 “连接” 事件。当有客户端连接上来 ,创建 子go程 与客户端进行通信
- 子go程:负责与客户端进行实时数据交互。并发。
实现流程
主go程中,创建监听器 Listener
使用 for 监听 listener —— Accept() —— conn
当有客户端发送连接请求时,创建 子go程。 使用 conn 与客户端进行数据通信
封装、实现函数:完成单个子go程与客户端的 read、write —— handleConnet(conn)
关闭当前go程 conn —— defer
for {
读取客户端的数据 conn.read()
获取客户端的 IP+port 。 conn.Remote().string()
处理数据 小——大 toUpper()
写回大写数据到客户端。
}
判断关闭
网络通信中,通过 read 返回值 为 0 判断对端关闭。
n, err := conn.read(buf) if n == 0 { // 检查到 conn 对端关闭。 } if err != nil && err != io.EOF { // 处理错误。 }
实现流程:
创建 与服务器通信 socket —— net.Dial()
创建 子go程循环监听 用户输入
go func () { for { os.Stdin.Read() } }()
将读到的数据写给 服务器 conn.Write
主go程 循环 读取服务器回发的 数据。
for { conn.read() }
将读到的数据打印到屏幕。
将 读到 的 “exit” 做为正常关闭连接标记。
- 在 服务器端,添加 “exit” 判断。 结束位置,包含 ‘\n’
if string(buf[:n]) == “exit\n”
TCP通信过程
三次握手:建立连接
主动发起连接请求端(客户端), 发SYN标志位,携带 序号。
被动接收连接请求端(服务器), 回复 ACK标志位 携带 确认序号, 发SYN标志位,携带 序号。
主动发起连接请求端(客户端),发送ACK标志位 携带 确认序号。
—— 第2个ACK发送完成。标志 三次握手完成。连接建立成功。
—— 服务器:Accept 函数返回。
—— 客户端:Dial 函数返回。
四次挥手:断开连接
主动关闭连接请求端(客户端), 发 FIN 标志位,携带序号。
被动关闭连接请求端(服务器),接收FIN标志位,发送 ACK 标志位。携带 确认序号。
—— 客户端 “半关闭” 完成。底层依赖 内核实心socket原理。
被动关闭连接请求端(服务器),发送 FIN 标志位, 携带序号。
主动关闭连接请求端(客户端),接收FIN标志位,发送 ACK 标志位。携带 确认序号。
—— 最后一个ACK被 被动关闭连接请求端 接收到,标志着,4次挥手完成。关闭连接完成。
- 滑动窗口:通知通信的对端,本地剩余存储空间的大小。
TCP状态转换
主动发起连接 (客户端):
CLOSED —— 发送 SYN —— SYN_SENT —— 接收 ACK 、SYN ,发送ACK —— ESTABLISHED(数据通信)
主动关闭连接(客户端):
ESTABLISHED —— 发送 FIN —— FIN_WAIT_1 —— 接收 ACK —— ==FIN_WAIT_2 (半关闭)== —— 接收 FIN、发送ACK —— TIME_WAIT —— 等待 2MSL 时长 —— CLOSED
- 2MSL作用: 确保 断开连接的 最后一个 ACK能被对端收到。等待一个固定时长(约40s)
- FIN_WAIT2、TIME_WAIT、2MSL 只出现在 “主动端”
被动接收连接 (服务器):
CLOSED —— LISTEN —— 接收 SYN,发送 ACK、SYN —— SYN_RCVD —— 接收 ACK —— ESTABLISHED
被动断开连接(服务器):
ESTABLISHED —— 接收 FIN ,发送ACK —— CLOSE_WAIT (对应半关闭的 FIN_WAIT_2) —— 发送 FIN —— LAST_ACK —— 接收 最后 ACK —— CLOSED
查看 网络通信状态 的命令 netstat
- 语法1: netstat -apn | grep 关键字(端口号)
- 语法2: lsof -i: 端口号
UDP通信
- TCP:面向连接的可靠的 流式数据包传递。 流式。 对不稳定的网络层做完全弥补。借助回执,丢包重传。
- UDP:无连接的 不可靠的 报文传输。报式。 对不稳定的网络层,直接还原真实状态。丢包不处理。
TCP | UDP | |
---|---|---|
优点 | 稳定(顺序、速度)、可靠 | 传输效率高、系统资源占用少、程序实现简单 |
缺点 | 传输效率低、系统资源占用多、程序实现复杂 | 不稳定(顺序、速度)、不可靠 |
使用场景 | 对数据的准确性、稳定性要求较高场合。上传、下载。 | 对数据实时性要求较高,可以适当允许数据丢失场合。游戏、视频、电话会议 |
UDP-CS-Server
实现流程:
获取UDP地址结构 —— UDPAddr
func ResolveUDPAddr(net, addr string) (*UDPAddr, error)
参数1:“udp”
参数2:“127.0.0.1:8000” IP+port
返回值:UDP地址结构
type UDPAddr struct { IP IP Port int Zone string // IPv6范围寻址域 }
绑定地址结构体,得到通信套接字 —— udpConn
func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error)
- 参数1:“udp”
- 参数2:ResolveUDPAddr 的返回值 : UDPAddr
- 返回值:用于通信的 socket
接收客户端发送数据 —— ReadFromUDP() —— n,cltAddr, err
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)
- 参数1:缓冲区
- 返回值:
- n:读到的实际字节数
- addr:对端的 地址结构(IP+Port)
写数据给客户端 —— WriteToUDP(数据,cltAddr) —— n, err
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
- 参数1:实际写出的数据
- 参数2:对端的地址
- 返回值:实际写出的字节数。
UDP-CS-Client
实现流程:
创建用于与服务器通信的 套接字 conn
net.Dial("udp", 服务器的IP+prot) --- conn
后续代码实现,参照 TCP-CS-Client。
UDP-CS-并发server
- UDP服务器,默认支持客户端并发访问。
网络文件传输
获取文件属性函数stat
func Stat(name string) (FileInfo, error)
type FileInfo interface { Name() string
Size() int64
Mode() FileMode
ModTime() time.Time
IsDir() bool
Sys() interface{}
}
命令行参数:
在main函数启动时,向整个程序传参。
语法: go run xxx.go argv1 argv2 argv3 argv4 。。。。
xxx.exe: 第 0 个参数。 argv1 :第 1 个参数。 argv2 :第 2个参数。 argv3 :第 3 个参数。 argv4 :第 4 个参数
使用: list := os.Args —— for idx, str := range list { fmt.println() 可以遍历 list }
参数 = list[3]
发送端 (客户端)
实现流程:
- 获取main命令行参数,得到文件 绝对路径 filePath
- 使用stat获取文件属性,得到 文件名(不带路径) fileName
- 与服务器建立连接 net.Dial(“tcp”, 服务器IP+port) ——conn
- conn.Write() 将文件名写给接收端(服务器)
- 读取接收端回发的 “ok”, 并判断。
- 封装、实现、调用函数。SendFile( 带路径文件名,conn )
- 只读 打开文件 Open - f
- 创建 缓冲区 存文件内容。buf
- 循环 读取文件内容,写给服务器。 读多少,写多少。
- 判断 f 结束 n == 0 。
- 关闭文件、conn。
接收端(服务器)
实现流程:
创建监听器 Listener
启动监听 Accept() -- conn
读取客户端发送的文件名,存储。
回发 “ok”
封装、实现、调用函数。RecvFile( 文件名,conn )
- 创建新文件 os.Create() - f
- 创建 缓冲区 存conn读到的文件内容。buf
- 循环写入本地文件
- 判断conn结束 n == 0 。
- 关闭文件、conn
Http概述
WEB工作方式:
- DNS服务器:转换 域名对应的 IP地址。
- 访问WEB服务器流程:访问 DNS服务器 —— IP地址 —— WEB服务器(http服务器)
Http协议(明文):
- 超文本传输协议。WEB通信,使用基本协议。
- https协议(加密): SSL、TLS (网景)
URL:
- 统一资源定位符。 定位到网络中某一个服务器上的某一个数据资源。
- 大致格式:协议(http)服务器名称(IP、域名)路径名、文件名。
HTTP协议格式
http请求协议格式
- 请求行:请求方法 “空格” 请求URL “空格” 协议及版本号 e.g. GET /itcast.txt HTTP/1.1
- 请求头:格式: key :value
- 空行:表示请求头结束。只使用 \r\n
- 请求包体:是否含有包体,取决于请求方法。 POST 方法含有包体。 GET 不含有包体。
http应答协议格式
- 应答行:协议版本号 “空格” 状态码 “空格” 状态描述 e.g. HTTP/1.1 200 ok
- 应答头:格式: key :value
- 空行:表示应答头结束。只使用 \r\n
- 应答包包体:
- 成功:请求的实际数据
- 失败:错误信息。(404 not found)
回调函数
- 实现本质:函数指针
- 函数指针语法:type 函数指针类型名 指向的函数原型(func 形参列表 返回值列表)。
- type FuncP func(x string, y bool)int
- 回调函数概念:
- 用户自定义一个函数,不直接调用,当满足某一特定条件时,有函数指针完成调用,或者由系统自动调用。
package main
import (
"fmt"
"unsafe"
)
// 定义函数指针类型
type FUNCP func (x int, y bool) int // FUNCP 数据类型. int / string /bool
// 指向一类函数(有两个参数(int/bool) 有一个 int 类型返回值)
func useCallback(x int, y bool, p FUNCP) int {
return p(x, y)
}
// 回调函数
func addOne(x int, y bool) int {
if y == true {
x += 1
}
return x
}
// 回调函数
func subTen(x int, y bool) int {
if y == true {
x -= 10
}
return x
}
func main() {
//addOne(10, true)
var p FUNCP
p = addOne
fmt.Printf("type=%T, size=%v\n", p, unsafe.Sizeof(p))
ret := useCallback(10, true, p)
fmt.Println("ret =", ret)
ret2 := useCallback(20, true, subTen)
fmt.Println("ret =", ret2)
}
Go语言实现的 HTTP服务器
注册回调函数
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
pattern:服务器提供给客户端访问 url(路径、文件)
handler 回调函数。—— 系统自动调用
func handler(w http.ResponseWriter, req *http.Request)
w: 回写给客户端的 数据(http协议)
req:读取到的客户端请求 信息
type ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(int) }
type Request struct { Method string // 浏览器请求方法 GET、POST… URL *url.URL // 浏览器请求的访问路径 …… Header Header // 浏览器请求头部 Body io.ReadCloser // 浏览器请求包体 RemoteAddr string // 浏览器地址 …… ctx context.Context }
启动服务
func ListenAndServe(addr string, handler Handler) error
- addr: 服务器地址结构(IP:port)
- handler :回调函数。通常传 nil —— 自动调用默认回调函数。DefaultServeMux
html简介
<html>
<head>
<title> 404 not found </title>
</head>
<body>
<center>
<h1> 404 NOT FOUND </h1>
</center>
<hr>
不好意思,你所请求的文件,不存在!
</body>
</html>
Go语言实现的 http 客户端
func Get(url string) (resp *Response, err error)
参数:待请求的 url。必须添加 http://
返回值:Response:
服务器回复的应答包。(应答行、应答头、应答包体)
type Response struct { Status string // e.g. "200 OK" StatusCode int // e.g. 200 Proto string // e.g. "HTTP/1.0" …… Header Header Body io.ReadCloser // 指针类型 —— read、close …… }
【结论】:在字串中匹配带有匹配参考项的 子串时, 使用 匹配参考项起始(?s:(.*?))匹配参考项结尾
网页爬虫
- 横向爬取:找寻网页的页与页之间的规律。分页器规律
- 纵向爬取:找寻一个页面中,条目与条目规律。
爬取流程
- 明确 url。 找寻网页分页器规律
http://tieba.baidu.com/f?kw=周杰伦&ie=utf-8&pn=0 --- 1
http://tieba.baidu.com/f?kw=周杰伦&ie=utf-8&pn=50 ---- 2
http://tieba.baidu.com/f?kw=周杰伦&ie=utf-8&pn=100
http://tieba.baidu.com/f?kw=周杰伦&ie=utf-8&pn=150 —— 下一页 + 50
- 获取网页数据。http.Get(url)
- 过滤有用数据信息。 正则、MustCompile、FindallstringSubmath
- 存储、保存数据。
横向规律:
https://movie.douban.com/top250?start=0&filter= ---1
https://movie.douban.com/top250?start=25&filter= ---2
https://movie.douban.com/top250?start=50&filter= ---3
https://movie.douban.com/top250?start=75&filter= ---4 —— 下一页 +25
纵向规律:
电影名称:
<img width="100" alt="(电影名称)"
评分:
<span class="rating_num" property="v:average">(分数)</span>
评价人数:
<span>(评价人数)人评价</span>
Go语言正则函数
- 编译解析正则表达式
regexp
func MustCompile(str string) *Regexp
- 参数:正则表达式字符串。推荐使用 反引号 ``
- 返回:编译后,go编译器能识别的 正则表达式 (struct)
- 利用正则,从字符串中,提取有用信息。
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string
参数1:需要过滤的 源字符串。
参数2:匹配次数。 -1 全部。
返回值:匹配成功的子串
匹配项中,行、列。 列【0】:带有 匹配项 列【1】:不带 匹配项 —— 使用!
默认情况下 “.” 能匹配任意一个字符,不能匹配 ‘\n’
(?s:(.*?)):
?s: 单行模式。 使 “.” 可以匹配 ‘\n’
.*?: 匹配 “.” , 越少越好匹配方法。
匹配 从 <div> 到 </div> 中间的数据
<div>xxx</div><div>yyy</div><div>zzz</div><div>qqq</div>
1 2 3 4 5 6 7 8
越多越好: 1-8
越少越好: 1-2
实现流程:
提示用户输入爬取起始、终止页start、end
跟据start、end 循环获取 url 下一页+25
封装、调用函数 HttpGET()获取一个网页全部数据,返回
- resp = http.Get() 获取web服务器的 http应答包
- defer resp.Body.Close()
- 创建缓冲区buf,循环读取body 存入。
- 创建 result 拼接 成 string 返回。
使用正则筛选数据
- 编译解析正则 mustCompile
- 提取数据
- 重复 12步,分别获取 电影名、评分、评价人数
保存成文件 Save2file(电影名、评分、评价人数 [][]string)
- 创建新文件 os.Create()
- 写 标题 : 电影名 \t\t 评分 \t\t 评价人数
- 循环 [][]string 依次按标题顺序,写入 数据。
起并发
- 创建 channel -- quit
- 封装一个网页的爬取步骤到 SpiderPage 函数中。
- go SpiderPage () 并发
- 使用 quit 协调主子go程 退出先后顺序(有N次写,有N次读)
图片下载
http://desk.zol.com.cn/bizhi/7407_91896_2.html --1
http://desk.zol.com.cn/bizhi/7407_91897_2.html --2
http://desk.zol.com.cn/bizhi/7407_91898_2.html --3
http://desk.zol.com.cn/bizhi/7407_91899_2.html --4
<img id="bigImg" src="==url==”
Json
序列化和反序列化
- 序列化:将数据转化为 字符串(json)类型存储
- 反序列化:解析 json 字符串,还原回成对应数据类型。
- 必要性:
- 数据发送端与接受端存在 平台差异(32位、64位)。
- 本机字节序、网络字节序存在 存储方式差异(大、小端法)
json 语法:
key:value:
- key:必须是 字符串,建议使用 “ ”
- value:任意类型:
- 整型
- 浮点型
- bool型
- null 空类型
- 对象 {}
- 数组 []
对象 {} :
成员可以是 :key:value 对,对象,数组。 用 “,”隔分。
数组 []:
成员可以是 :key:value 对,对象,数组。 用 “,”隔分。
{ "Name":"张三丰", "Age":180, "Marray":false, "家电":[{"剃须刀":["吉列","飞利浦"]}, {"电视机":["三星"]}] }
在线工具:www.json.cn
序列化
enconding/json
func Marshal(v interface{}) ([]byte, error)
- 参数:待序列化的 数据
- 返回值:json串
结构体序列化
- 要求 结构体类型名 和 成员(属性)名 必须大写(包作用域), 才能在 json包中可见。
- 否则,不报错!!! 但不做序列化转换。
{"Name":"张三","Age":29,"Score":98.9,"Id":1001}
map序列化
- 定义的map变量,不使用 make 初始化,不能存储数据。
{"Age":19,"Id":9527,"Name":"华安","food":["鸡翅膀","含笑半步癫","一日丧命散"]}
Slice序列化
- 定义 slice 变量,可以不初始化,直接使用 append 向其中,添加数据。
[{"Age":19,"Id":9527,"Name":"华安","food":["鸡翅膀","含笑半步癫","一日丧命散"]},{"Age":16,"Id":521,"Name":"秋香","food":["鸡翅膀","一日丧命散"],"教师":{"Name":"石榴姐","Age":30,"Addr":"华府"}}]
结构体标签
用途:在序列化时,重新指定 json key 值
语法:使用 反引号 `` 包裹整个结构体标签。 key : value
- key : json、自定义key
- value:必须使用 “ ” 包裹 value 值。 ==否则,Tag标签不起作用,且不产生错误!==
- value中,如果有多个值,使用“,”隔分,==不能随意添加空格==。
type Student5 struct { Name string `json:"-"` // "-"作用,不进行序列化.显示结果与小写成员名一致. Age int `json:"age,omitempty"` // 序列化时, 忽略0值 / 空值. Score float64 `json:"score,string"` // 序列化时, 转换为 string Id int `json:"id"` } 输出结果:{"score":"99.9","id":1002}
反序列化
func Unmarshal(data []byte, v interface{}) error
- 参数1:序列化后生产的json串
- 参数2:用来传出反序列化后的 原始数据。使用时 传指针。
- 返回值:一旦err有数据,说明反序列化失败。
结构体反序列化
- 用接收反序列化的 结构体类型,必须与原序列化的结构体类型 严格一致(成员个数、类型)
- Unmarshal(&struct)
map反序列化
如果接收反序列化结果的 map 没有开辟空间,Unmarshal 函数,帮助自动开辟。
虽然 map 数据类型为“引用”,但使用 Unmarshal函数时,必须要&传参。否则报错。
Unmarshal(&map)
slice反序列化
- Unmarshal(&slice)
网络聊天室
聊天室模块划分
主go程(服务器)
监听客户端连接请求、启动go程(HandleConnet)与客户端通信、创建 Manger go程(管理全局Message)
HandleConnet go程
添加新用户到在线用户列表;组织用户上线消息,写给全局Message;创建 监听用户自带channel的go程(WriteMsgToClient);发送用户聊天消息、查询在线用户、修改用户名、用户下线、超时强踢。
Manager 管理者 go程
监听全局Message上是否有数据(读)。遍历在线用户列表,将读到数据写给用户自带channel。
WriteMsgToClient go程
监听 用户自带channel 上是否有数据可读。读到写给用户。
其他资源
- 用户结构体 Client { Name, IP+port, channel } 新用户 Name == IP+port
- 全局 channel (Message)。 所有需要 “广播” 的消息,写入本 channel
- 全局在线用户列表。onlineMap key:IP+port value:Client
广播用户上线
图示分析
实现流程
创建监听器 net.listen() --- listener
for 循环监听客户端连接 --- listener.Accept() --- conn
创建全局 Manager go程, 读全局channel , 遍历在线用户
创建客户端结构体 Client {Name、Addr、C}
创建全局 channel -- Message
创建 在线用户列表 onlineMap key:IP+port(string) value : Client
实现 Manager
for {
- 读全局 <-Message。 无数据, 阻塞; 有数据,继续向下遍历
- 遍历 onlineMap。 将 读到的消息,写给 用户自带的 C
}
创建、实现、调用子go程 handleConnet(conn)处理客户端事件
defer conn.Close() // 每个客户端结束时,关闭对应socket
获取客户端地址结构 conn.Remoteaddr().String()
初始化客户端对象。 Name == Addr == IP + port
添加新用户 到 全局 在线用户列表
创建、实现、调用 go程 WriteMsgToClient(clt, conn) 监听用户自带 C 上是否有数据
for {
- 读用户自带 msg := <-clt.C。 无数据, 阻塞; 有数据,继续向下写给客户端
- conn.Write(msg)
}
组织用户上线消息。loginMsg := "[" + clt.Addr + "]" + clt.Name + ":" + "上线!"
写给 全局Message 。 广播!!!
添加测试代码,防止当前go程退出。 for { runtime.GC() }
添加读写锁 —— 保护在线用户列表
- 全局位置创建 rwMutex
- 在 Manager go程中,遍历在线用户列表前,加读锁
- 遍历结束 ,解读锁。
- 在 handleConnet go程中,添加新用户 到 全局 在线用户列表 前 ,加 写锁
- 添加结束, 解写锁。
广播用户聊天消息
实现流程:
- 在 handleConnet go程中,完成上线广播后,创建 匿名go程,用来读取客户端消息。
- for 循环读取客户端数据。--- buf
- 判断关闭、判断异常
- 封装 makeMsg(clt, str)string 组织消息格式。(改写之前用户上线消息)
- 使用 makeMsg 组织用户聊天信息。
- 写入 全局Message 。 广播!!!
查询在线用户列表
实现流程:
- 截取用户信息,去除最后一个字符‘\n’.。 保存成 msg
- 读取数据后,判断 用户是否 发送 “who” 指令。如果不是,广播聊天消息。
- 变量全局在线用户列表。
- 组织在线用户信息
- 写给当前用户。不广播!!!conn.Write
- 添加读写锁的 读加锁、读解锁。保护共享 全局在线用户列表。
修改用户名
实现流程:
- 判断 用户是否 发送 “rename|” 指令。 如果不是,广播聊天消息。
- 利用 && 短路运算特性。先判断 len(msg) > 7 再判断 msg[:7] == “rename|”
- 如果先判断 msg[:7], 会修改msg切片数据。
- 使用 split 函数,按 “|” 拆分用户信息,获取新用户名
- 更新用户结构体,新名覆盖旧名。
- 更新 在线用户列表。更新含有新用户名的用户。
- 通知当前用户改名成功。不广播!!!conn.Write。
- 添加 写锁、保护在线用户列表。
用户下线:
实现流程:
- 在 handleConnet go程中,匿名go程之前。 创建 channel 管理用户下线。isQuit
- conn.Read == 0 时,确认用户下线, isQuit <- true。 return 退出当前匿名go程
- 在 handleConnet go程结尾的 for 中,添加 select (替换 runtime.GC())
- 在 一个 Case 中监听 <-isQuit :
- 关闭 用户自带 channel ,促使 WriteMsgToClient go程结束。
- 将 WriteMsgToClient go程 内读取用户 channel 的 for 改为 for range 遍历 C
- 从在线用户列表中,删除当前用户。delete()
- 组织用户下线消息。广播!写给 全局Message 。
- return 退出 handleConnet go程
- 使用写锁,保护在线用户列表
用户超时强踢
目的:长时间连接服务器,不发送消息的 客户端,踢除,释放服务器资源。
实现流程:
添加select 的 case 分支,监听 time.After()
当定时满时,释放用户资源:
- 关闭 用户自带 channel ,促使 WriteMsgToClient go程结束。
- 将 WriteMsgToClient go程 内读取用户 channel 的 for 改为 for range 遍历 C
- 从在线用户列表中,删除当前用户。delete()
- 组织用户下线消息。广播!写给 全局Message 。
- return 退出 handleConnet go程
- 使用写锁,保护在线用户列表
在 handleConnet go程中,匿名go程之前。 创建 channel 重置定时器。isLive
添加 select 的 case 分支,监听 <-isLive 。 当isLive上有写入时,定时器会重新计时。
在用户 “广播聊天”、“查询在线用户who”、“修改用户名 rename” 任意一个分支 之后,添加 isLive <-true