Go面向对象编程
1、Golang 语言面向对象编程说明
不是纯粹的面向对象语言Golang 支持面向对象编程特性没有类(class)Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位基于 struct 来实现 OOP 特性去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针Golang 没有extends 关键字继承是通过匿名字段来实现
2、结构体
2.1、声明结构体
// 基本语法
type 结构体名称 struct {
field1 type
field2 type
}
//举例:
type Student struct {
Name string //字段
Age int //字段
Score float32
}
2.2 字段 / 属性
基本介绍
- 从概念或叫法上看: 结构体字段 = 属性 = field
- 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体 的 Name string 就是属性。
注意事项和说明
布尔类型是 false ,数值是 0 ,字符串是 ""指针,slice,和 map 的零值都是 nil ,即还没有分配空间结构体是值类型
package main
import (
"fmt"
)
type NilChecker struct {
ptr *int
slice []string
mmp map[string]string
}
func main() {
// 测试默认值
var check NilChecker
// 1、指针
fmt.Printf("ptr地址 %p\t", check.ptr)
fmt.Printf("ptr地址 %t\n", check.ptr == nil)
// 2、切片
fmt.Printf("slice地址 %p\t", check.slice)
fmt.Printf("slice地址 %t\n", check.slice == nil)
// 3、map
fmt.Printf("mmp地址 %p\t", check.mmp)
fmt.Printf("mmp地址 %t\n", check.mmp == nil)
}
package main
import (
"fmt"
)
type Stu struct {
Name string
age int
}
func main() {
// 测试结构体是值类型
var sst Stu
sst.Name = "kiko"
sst.age = 12
fmt.Println("sst : ", sst)
sst_1 := sst
sst_1.Name = "yoyo"
sst_1.age = 15
fmt.Println("sst_1 : ", sst_1)
fmt.Println("sst : ", sst)
}
2.2 创建结构体变量和访问结构体字段
方式1 直接声明
var person Person
方式2
var person Person = Person{} // 如果需要,可以在{}给字段赋值
package main
import (
"fmt"
)
type Stu struct {
Name string
age int
}
func main() {
// 三种形式都可以
var st Stu = Stu{}
fmt.Println(st)
st = Stu{"kiko", 12}
fmt.Println(st)
st = Stu{Name: "kiko", age: 12}
fmt.Println(st)
}
方式3 &
var person *Person = new (Person)
package main
import (
"fmt"
)
type Stu struct {
Name string
age int
}
func main() {
// 创建指针
var st *Stu = new(Stu)
(*st).Name = "yoyo"
(*st).age = 19
fmt.Println(st)
st.Name = "kiko" // 可以解引用,go设计者做了处理
st.age = 18
fmt.Println(st)
}
方式4 &{}
// 结合了第二种方式和第三种方式
var person *Person = &Person{} // {}和上面一样可以给字段赋值
四种方式说明
也支持 结构体指针.字段名不能写成*p.Name
2.3 结构体使用注意事项和细节
- 结构体的所有字段在内存中是连续的
package main
import (
"fmt"
)
type Point struct {
x int32
y int64
}
type Rect struct {
leftTop, rightTop Point
}
type Triangle struct {
vertex, gravity *Point
}
func main() {
var rect Rect = Rect{
Point{1, 2},
Point{3, 4},
}
fmt.Printf("%p, %p\n", &(rect.leftTop), &(rect.rightTop))
fmt.Printf("%p, %p, %p, %p\n", &(rect.leftTop.x), &(rect.leftTop.y), &(rect.rightTop.x), &(rect.rightTop.y))
tri := Triangle{
&Point{10, 20},
&Point{30, 40},
}
fmt.Printf("%p, %p\n", &(tri.vertex), &(tri.gravity)) // 本身的地址是连续的
fmt.Printf("%p, %p\n", tri.vertex, tri.gravity) // 指向的地址是不连续的
fmt.Printf("%p, %p, %p, %p\n", &(tri.vertex.x), &(tri.vertex.y), &(tri.gravity.x), &(tri.gravity.y))
}
需要有完全相同的字段(名字、个数和类型)tag结构体中的字段名如果不大写开头,那么就不能在别的包中使用,但是如果大写了,面对一些例如序列化成json串的时候,它的键的首字母就会是大写,因此出现tag标签功能
package main
import (
"encoding/json"
"fmt"
)
type Monster struct {
Name string `json:"name"`
Age int `json:"age"`
Skill string `json:"skill"`
}
func main() {
// 1、创建一个Monster变量
monster := Monster{"孙悟空", 500, "金箍棒"}
// 2、将monster变量序列化为json字符串
// json.Marshal 函数中使用反射,实现结构体转字符串
jsonStr, err := json.Marshal(monster)
if err != nil {
fmt.Println("json 处理错误 ", err)
}
// {"Name":"孙悟空","Age":500,"Skill":"金箍棒"}
// 显然字符串的Name等都是大写的,不符合json串的阅读习惯和书写习惯
// {"name":"孙悟空","age":500,"skill":"金箍棒"}
// 结构体字段后面添上tag后,字段名的首字母就变成了小写
fmt.Println("jsonStr: ", string(jsonStr))
}
3、方法
3.1 基本介绍
作用在指定的数据类型上的和指定的数据类型绑定
3.2 方法快速入门
package main
import (
"fmt"
)
type Person struct {
name string
age int
}
// 声明定义方法
// 方法一、直接输出person的姓名
func (person Person) sayHello() {
fmt.Println(person.name, " say Hello...")
}
// 方法二、计算传入的两个值,并输出结果
func (person Person) calculate(num1 int, num2 int) {
fmt.Println(person.name, " 计算 ", " 结果为 = ", num1+num2)
}
// 方法三、计算0-num的累加值,并且返回
func (person Person) caloneTtonums(num int) int {
sum := 0
for i := 0; i <= num; i++ {
sum += i
}
return sum
}
// 方法四、返回姓名和年龄
func (person Person) retNameAge() (string, int) {
fmt.Printf("%p\n", &person)
return person.name, person.age
}
func main() {
// 创建一个person对象
var person Person = Person{"kiko", 19}
// 用于调用方法的两个person是否是同一个对象
fmt.Printf("%p\n", &person)
// 方法调用
fmt.Println(person.retNameAge())
fmt.Println(person.caloneTtonums(100))
person.sayHello()
person.calculate(10, 20)
}
说明
p 就是它的副本, 这点和函数传参非常相似。同样的,这里的p和调用的方法的对象不是一个东西,这是一个值拷贝
3.3 方法的调用和传参机制原理
方法调用时,会将调用方法的变量,当做实参也传递给方法值类型就是值拷贝,引用类型就是地址拷贝
3.4 方法的声明(定义)
func (recevier type) methodName(参数列表) (返回值列表){
方法体
return 返回值
}
1) 参数列表:表示方法输入
2) recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
3) receiver type : type 可以是结构体,也可以其它的自定义类型
4) receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
5) 返回值列表:表示返回的值,可以多个
6) 方法主体:表示为了实现某一功能代码块
7) return 语句不是必须的。
3.5 方法的注意事项和细节
遵守值类型的传递机制,是值拷贝传递方式可以通过结构体指针的方式来处理自定义类型,都可以有方法
type Integer int // 需要重新定义类型,变为自定义类型
func (v *Integer) changeValue() {
(*v) = 999
}
func main() {
var v Integer = 100
(&v).changeValue()
fmt.Println(v) // 999
}
方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问
package main
import (
"fmt"
)
type Person struct {
name string
age int
}
func (person *Person) String() string {
str := fmt.Sprintf("name = [%v] age = [%v]", person.name, person.age)
return str
}
func main() {
person := Person{
name: "yoyo",
age: 18,
}
fmt.Println(&person)
}
3.6 方法和函数的区别
- 调用方式不一样
函数的调用方式: 函数名(实参列表)
方法的调用方式: 变量.方法名(实参列表)
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。
- 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。
- 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定。
- 如果是和值类型,比如 (p Person) , 则是值拷贝, 如果和指针类型,比如是 (p *Person) 则是地址拷贝。
4、工厂模式
没有构造函数,通常可以使用工厂模式来解决这个问题
需求引入
如果首字母是小写,那么就无法在其他包中创建对象
package model
type student struct {
name string
age int
}
// 创建一个Student对象,返回值类型
func InstanceStu1(name1 string, age1 int) student {
return student{
name: name1,
age: age1,
}
}
// 创建一个Student地址对象,返回地址
func InstanceStu2(name1 string, age1 int) *student {
return &student{
name: name1,
age: age1,
}
}
// 方法,让包外的代码操作字段
func (stu *student) GetStuName() string {
return stu.name
}
func (stu *student) SetStuName(name string) {
stu.name = name
}
func (stu *student) GetStuAge() int {
return stu.age
}
func (stu *student) SetStuAge(age int) {
stu.age = age
}
5、面向对象三大特性之封装
基本介绍封装介绍把抽象出的字段和对字段的操作封装在一起,数据被保护在内部
封装的理解和好处
- 隐藏实现细节
- 可以对数据进行验证,保证安全合理(比如要求设置的Age要大于0小于125)
如何体现封装
- 对结构体中的属性进行封装
- 通过方法,包 实现封装
封装的实现步骤
- 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
- 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
- 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
func (varia 结构体类型名) SetXxx(参数列表) (返回值列表) {
//加入数据验证的业务逻辑
varia .字段 = 参数
}
- 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
func (varia 结构体类型名) GetXxx() {
return varia.age;
}
package model
import "fmt"
type person struct {
Name string
age int // 其他包不能直接访问age和salary
salary float32
}
// 写一个工厂模式的构造
func CreateObj(name string) *person {
return &person{
Name: name,
age: 0,
salary: 0.0,
}
}
// 为了访问age 和salary,编写GetXxx和SetXxx
func (p *person) SetAge(age int) {
if age < 0 || age > 100 {
fmt.Println("age设置的值不合法")
} else {
p.age = age
}
}
func (p *person) GetAge() int {
return p.age
}
func (p *person) SetSalary(salary float32) {
if salary < 0 || salary > 10000000 {
fmt.Println("salary设置的值不合法")
} else {
p.salary = salary
}
}
func (p *person) GetSalary() float32 {
return p.salary
}
6、面向对象三大特性之继承
基本介绍解决代码复用结构体中定义这些相同的属性和方法匿名结构体golang中通过匿名结构体来实现继承
在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
基本语法
继承的使用
package main
import (
"fmt"
)
// 定义Student结构体和相关方法
type Student struct {
name string
age int
score float32
}
func (stu *Student) ShowInfo() {
fmt.Printf("学生名 = %v 年龄 = %v, 成绩 = %v\n", stu.name, stu.age, stu.score)
}
func (stu *Student) SetScore(score float32) {
stu.score = score
}
// 小学生结构体
type Pupil struct {
Student
}
// 结构体Pupil绑定的特有方法
func (p *Pupil) testing() {
fmt.Println("小学生考试......")
}
// 大学生结构体
type Graduate struct {
Student
}
// 结构体Graduate绑定的特有的方法
func (g *Graduate) testing() {
fmt.Println("大学生测试......")
}
func main() {
// 小学生
puil := &Pupil{}
puil.Student.name = "kiko"
puil.Student.age = 8
puil.testing()
puil.Student.SetScore(90)
puil.Student.ShowInfo()
fmt.Println("----------------------------------------------------")
// 大学生
graduate := &Graduate{}
graduate.Student.name = "yoyo"
graduate.Student.age = 18
graduate.testing()
graduate.Student.SetScore(130)
graduate.Student.ShowInfo()
}
继承深入探讨
结构体可以使用嵌套匿名结构体所有的字段和方法当然上面的说法仅限于两个结构体在同一个包中
type A struct {
Name string
age int
}
func (a *A) SayOk() {
fmt.Println("A SayOk ", a.Name)
}
func (a *A) SayHello() {
fmt.Println("A SayHello ", a.Name)
}
type B struct {
A
}
func main() {
var b B
b.A.Name = "kiko"
b.A.age = 10
b.A.SayOk()
b.A.SayHello()
// 上面的写法可以简化
b.Name = "yoyo"
b.age = 20
b.SayOk()
b.SayHello()
}
小结
当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问结构体嵌入两个(或多个)匿名结构体(多重继承,不建议使用)同时结构体本身没有同名的字段和方法
type A struct {
Name string
age int
}
type B struct {
Name string
score int
}
type C struct {
A
B
// C结构体本身没有Name字段
}
func main() {
var c C
// 如果C结构体本身有Name字段,那么c.Name是正确的,给C本身的额赋值
// c.Name = "kiko" // 错误,指向不明确
c.A.Name = "yoyo" // 正确
}
- 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字。这种模式就不是继承关系,只能当做一个成员,按照普通的成员使用即可。
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值。
type Goods struct {
Name string
Price float32
}
type Brand struct {
Name string
Address string
}
type TV struct {
Goods
Brand
}
// 也可以使用这种地址方式的匿名结构体
type Computer struct {
*Goods
*Brand
}
func main() {
tv := TV{
Goods{
Name: "电视机",
Price: 10000,
},
Brand{
Name: "海尔",
Address: "全世界",
},
}
fmt.Println(tv)
computer := Computer{
&Goods{
Name: "计算机",
Price: 10000,
},
&Brand{
Name: "海尔",
Address: "全世界",
},
}
fmt.Println(computer)
fmt.Println(*(computer.Goods), *(computer.Brand))
}
7、接口(面向接口编程)
多态特性主要是通过接口来体现
7.1 接口入门案例
计算机有很多USB接口,不同的设备插入这个USB借口,计算机能够识别且执行不同的功能。
package main
import (
"fmt"
)
// 定义一个接口
type USB interface {
start()
stop()
}
// 定义结构体
type Camera struct {
}
type Mouse struct {
}
// 实现接口方法
func (c *Camera) start() {
fmt.Println("Camera started...")
}
func (c *Camera) stop() {
fmt.Println("Camera stopped...")
}
func (m *Mouse) start() {
fmt.Println("Mouse started...")
}
func (m *Mouse) stop() {
fmt.Println("Mouse stopped...")
}
// 计算机的结构体
type Computer struct {
}
func (computer *Computer) Run(usb USB) {
usb.start()
usb.stop()
}
func main() {
computer := &Computer{}
m := &Mouse{}
c := &Camera{}
// 能够传入Run参数的是要求Mouse和Camera实现USB接口的所有方法
// 如果Mouse和Camera结构体没有实现接口的所有方法会报错
computer.Run(m)
computer.Run(c)
}
7.2 接口概念
可以定义一组方法不需要实现,也不能在内部实现不能包含任何变量
说明:
程序设计的多态和高内聚低偶合只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口没有 implement 这样的关键字
7.3 注意事项和细节
本身不能创建实例所有的方法都没有方法体一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口只要是自定义数据类型,就可以实现接口interface 类型默认是一个指针(引用类型)接口是引用类型,当将实现了接口的结构体赋值给接口变量时,结构体创建的实例必须是返回引用的方式所以所有类型都实现了空接口
package main
import (
"fmt"
)
// 空接口就是没有任何的方法声明的一个接口,
// 任何类型的变量都可以赋值给空接口类型的变量
type Mouse struct {
}
// 定义一个空接口
type T interface {
}
func main() {
// 1、使用已经定义好的接口类型T
var mouse T = Mouse{}
fmt.Println(mouse)
var i int = 10
mouse = i
fmt.Println(mouse)
// 2、直接定义空接口
var E interface{} = mouse
fmt.Println(E)
}
- 两种特殊的情况
package main
import (
"fmt"
)
/*
1、情况一
两个接口A、B,两个接口有两个方法,其中一个方法的名称是一样的;还有一个结构体C,
结构体实现这两个接口
*/
type AInterface interface {
SayHello()
SayOk1()
}
type BInterface interface {
SayHello()
SayOk2()
}
type Runner struct {
name string
}
// 结构体实现接口的方法.
// 这种情况,同名的只需要实现一个即可
func (runner *Runner) SayHello() {
fmt.Println(runner.name, " say hello")
}
func (runner *Runner) SayOk1() {
fmt.Println(runner.name, " say ok1")
}
func (runner *Runner) SayOk2() {
fmt.Println(runner.name, " say ok2")
}
func main() {
var a AInterface = &Runner{"kiko"}
a.SayHello()
a.SayOk1()
var b BInterface = &Runner{"yoyo"}
b.SayHello()
b.SayOk2()
}
package main
import (
"fmt"
)
/*
2、情况二
有三个接口ABC,其中AB接口有两个方法,其中一个是方法名相同的
接口C继承了AB
*/
type AInterface interface {
SayHello()
SayOk1()
}
type BInterface interface {
SayHello()
SayOk2()
}
type CInterface interface {
AInterface
BInterface
}
type Runner struct {
name string
}
func (runner *Runner) SayHello() {
fmt.Println(runner.name, "Hello world")
}
func (runner *Runner) SayOk1() {
fmt.Println(runner.name, "SayHello1")
}
func (runner *Runner) SayOk2() {
fmt.Println(runner.name, "SayHello2")
}
func main() {
var runner CInterface = &Runner{"kiko"}
runner.SayOk1()
runner.SayOk2()
runner.SayHello()
}
7.4 继承和接口实现
package main
import (
"fmt"
)
type Monkey struct {
name string
}
func (this *Monkey) climb() {
fmt.Println(this.name, " 生来就会爬树...")
}
type FlyAble interface {
canFly()
}
type SwimAble interface {
canSwim()
}
type SmartMonkey struct {
Monkey // 继承Monkey结构体
}
// 让SmartMonkey实现两个接口
func (this *SmartMonkey) FlyAble() {
fmt.Println(this.name, " 是一个聪明的猴子,通过学习能够飞翔")
}
func (this *SmartMonkey) FishAble() {
fmt.Println(this.name, " 是一个聪明的猴子,通过学习能够游泳")
}
func main() {
monkey := SmartMonkey{
Monkey{
name: "yoyo",
},
}
monkey.climb()
monkey.FlyAble()
monkey.FishAble()
}
代码小结:
实现接口是对继承机制的补充
解决的问题不同复用性和可维护性
继承是满足 is-a 的关系,而接口只需满足 like-a 的关系
7.5 接口编程的最佳实践
func Sort(data Interface)Interface接口
type Interface interface {
// Len方法返回集合中的元素个数
Len() int
// Less方法报告索引i的元素是否比索引j的元素小
Less(i, j int) bool
// Swap方法交换索引i和j的两个元素
Swap(i, j int)
}
切片方式
package main
import (
"fmt"
"math/rand"
"sort"
)
// 1、声明Hero结构体
type Hero struct {
name string
age int
}
// 2、声明一个Hero结构体切片类型(这里也可以定义成数组,只要能够实现Interface的三个方法即可)
type HeroSlice []Hero
// 3、切片类型HeroSlice实现Interface接口
func (hero HeroSlice) Len() int {
return len(hero) // 返回集合中的元素个数
}
func (hero HeroSlice) Less(i, j int) bool {
return hero[i].age < hero[j].age //报告索引i的元素是否比索引j的元素小
}
func (hero HeroSlice) Swap(i, j int) { // 如果Less返回为真,则交换
// temp := hero[i]
// hero[i] = hero[j]
// hero[j] = temp
// 上面三句交换代码,可以简写为下面一句
hero[i], hero[j] = hero[j], hero[i]
}
func main() {
var heroes HeroSlice
for i := 0; i < 10; i++ {
hero := Hero{
name: fmt.Sprintf("英雄--> %d", rand.Intn(100)),
age: rand.Intn(100),
}
// 将hero append到heroes切片中
heroes = append(heroes, hero)
}
// 1、输出排序前的结构体
for i := 0; i < len(heroes); i++ {
fmt.Printf("%v\t", heroes[i])
}
fmt.Println()
// 2、调用sort.Sort(data Interface)函数排序
sort.Sort(heroes)
// 3、输出排序后的结果
// for i := 0; i < len(heroes); i++ {
// fmt.Printf("%v\t", heroes[i])
// }
for _, val := range heroes {
fmt.Printf("%v\t", val)
}
}
数组方式
package main
import (
"fmt"
"math/rand"
"sort"
)
// 1、声明Hero结构体
type Hero struct {
name string
age int
}
// 2、声明一个Hero结构体切片类型(这里也可以定义成数组,只要能够实现Interface的三个方法即可)
type HeroArray [10]Hero
// 3、切片类型HeroSlice实现Interface接口
func (hero *HeroArray) Len() int {
return len(hero) // 返回集合中的元素个数
}
func (hero *HeroArray) Less(i, j int) bool {
return hero[i].age < hero[j].age //报告索引i的元素是否比索引j的元素小
}
func (hero *HeroArray) Swap(i, j int) { // 如果Less返回为真,则交换
// temp := hero[i]
// hero[i] = hero[j]
// hero[j] = temp
// 上面三句交换代码,可以简写为下面一句
hero[i], hero[j] = hero[j], hero[i]
}
func main() {
var heroes HeroArray
for i := 0; i < 10; i++ {
hero := Hero{
name: fmt.Sprintf("英雄--> %d", rand.Intn(100)),
age: rand.Intn(100),
}
// 将hero append到heroes切片中
// heroes = append(heroes, hero)
heroes[i] = hero
}
// 1、输出排序前的结构体
for i := 0; i < len(heroes); i++ {
fmt.Printf("%v\t", heroes[i])
}
fmt.Println()
// 2、调用sort.Sort(data Interface)函数排序
sort.Sort(&heroes)
// 3、输出排序后的结果
// for i := 0; i < len(heroes); i++ {
// fmt.Printf("%v\t", heroes[i])
// }
for _, val := range heroes {
fmt.Printf("%v\t", val)
}
}
8、面向对象三大特性之多态
8.1 基本介绍
在 Go 语言,多态特征是通过接口实现的可以按照统一的接口来调用不同的实现
8.2 快速入门
比如 Usb 接口,Usb usb ,既可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口 多态特性。(接口入门案例)
8.3 接口体现多态的两种形式
1、多态参数
在前面的 Usb 接口案例,Usb usb ,即可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口 多态。(接口入门案例就是这种形式)
2、多态数组
演示一个案例:给 Usb 数组中,存放 Phone 结构体 和 Camera 结构体变量。
9、类型断言
9.1 引入
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言。
*** 对上面代码的说明:
在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.
如何在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic。
9.2 断言最佳实践1
package main
import (
"fmt"
)
// 定义一个接口
type USB interface {
start()
stop()
}
// 定义结构体
type Camera struct {
name string
}
type Mouse struct {
name string
}
// 实现接口方法
func (c *Camera) start() {
fmt.Println(c.name, " Camera started...")
}
func (c *Camera) stop() {
fmt.Println(c.name, " Camera stopped...")
}
func (m *Mouse) start() {
fmt.Println(m.name, " Mouse started...")
}
func (m *Mouse) stop() {
fmt.Println(m.name, " Mouse stopped...")
}
// 定义一个方法,且是Camera结构体自有的
func (c *Camera) snapshot() {
fmt.Println(c.name, " Camera snapshot started...")
}
// 计算机的结构体
type Computer struct {
}
func (computer *Computer) Run(usb USB) {
usb.start()
// 类型断言
// 如果能够转换为Camera,那么就执行它自有的结构体方法
if obj, ok := usb.(*Camera); ok {
obj.snapshot()
}
usb.stop()
}
func main() {
computer := &Computer{}
var usbArr [3]USB
usbArr[0] = &Mouse{"极光鼠标"}
usbArr[1] = &Camera{"太阳照相机"}
usbArr[2] = &Mouse{"南极鼠标"}
for i := 0; i < 3; i++ {
computer.Run(usbArr[i])
}
}
9.3 断言最佳实践2
判断变量的类型
func TypeJudge(items ...interface{}) {
for index, val := range items {
switch val.(type) { // 固定写法type是关键字
case bool:
fmt.Printf("第%v个参数是bool类型,值是%v\n", index, val)
case float32:
fmt.Printf("第%v个参数是float32类型,值是%v\n", index, val)
case float64:
fmt.Printf("第%v个参数是 float64类型,值是%v\n", index, val)
case int, int32, int64:
fmt.Printf("第%v个参数是整数类型,值是%v\n", index, val)
case string:
fmt.Printf("第%v个参数是string类型,值是%v\n", index, val)
default:
fmt.Printf("第%v个参数是类型不确定,值是%v\n", index, val)
}
}
}
9.4 断言最佳实践3
在2的基础上增加Student和*Student类型判断
type Student struct {
name string
age int
}
func TypeJudge(items ...interface{}) {
for index, val := range items {
switch val.(type) { // 固定写法type是关键字
case bool:
fmt.Printf("第%v个参数是bool类型,值是%v\n", index, val)
case float32:
fmt.Printf("第%v个参数是float32类型,值是%v\n", index, val)
case float64:
fmt.Printf("第%v个参数是 float64类型,值是%v\n", index, val)
case int, int32, int64:
fmt.Printf("第%v个参数是整数类型,值是%v\n", index, val)
case string:
fmt.Printf("第%v个参数是string类型,值是%v\n", index, val)
case Student:
fmt.Printf("第%v个参数是student类型,值是%v\n", index, val)
case *Student:
fmt.Printf("第%v个参数是*student类型,值是%v\n", index, val)
default:
fmt.Printf("第%v个参数是类型不确定,值是%v\n", index, val)
}
}
}
func main() {
TypeJudge(100, true, 34.9, "str", Student{name: "yoyo", age: 18}, &Student{name: "kiko", age: 19})
}
Go文件操作
1、文件的基本介绍
文件概念
输入流和输出流
os.File所有文件相关操作结构体
2、打开和关闭文件
函数方法
打开文件(函数):
func Open(name string) (file *File, err error)
Open打开一个文件用于读取。
如果操作成功,返回的文件对象的方法可用于读取数据;对应的文件描述符具有O_RDONLY模式。
如果出错,错误底层类型是*PathError。
源码:
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0) // Open方法只有读权限
}
关闭文件(方法):
func (f *File) Close() error
Close关闭文件f,使文件不能用于读写。它返回可能出现的错误。
*** 测试案例
如果文件不存在会报错
package main
import (
"fmt"
"os"
)
func main() {
// 打开文件
// 返回的是一个file地址
file, err := os.Open("G:/Golang/GoProjects/files/test.txt")
if err != nil {
fmt.Println("open file err =", err)
}
fmt.Println(file)
// 关闭文件
err = file.Close()
if err != nil {
fmt.Println("close file err =", err)
}
}
3、读取文件
带缓冲的读取
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
// 打开文件
file, err := os.Open("G:/Golang/GoProjects/files/test.txt")
if err != nil {
fmt.Println("open file err =", err)
}
defer file.Close() // 延迟,当函数结束时,关闭文件,否则会有内存泄漏
// 创建一个 *Reader ,是带缓冲的
/*
const (
defaultBufSize = 4096 //默认的缓冲区为 4096
)
*/
reader := bufio.NewReader(file)
// 循环读取文件的内容
for {
str, err := reader.ReadString('\n') //读到换行结束
if err == io.EOF {
break
}
fmt.Print(str)
}
fmt.Println("文件读取结束...")
}
一次性读取适用于文件不大的情况ioutil.ReadFile
func ReadFile(filename string) ([]byte, error)
ReadFile 从filename指定的文件中读取数据并返回文件的内容。
成功的调用返回的err为nil而非EOF。
因为本函数定义为读取整个文件,它不会将读取返回的EOF视为应报告的错误。
package main
import (
"fmt"
"io/ioutil"
)
func main() {
// 使用ioutil.ReadFile一次性将文件读取到位
file := "G:/Golang/GoProjects/files/test.txt"
content, err := ioutil.ReadFile(file) // 返回的是[]byte
if err != nil {
fmt.Printf("read file err = %v", err)
}
// 把读取到的内容显示到终端
fmt.Printf("%v", string(content))
//代码中没有显式的open文件,因此也不需要显式的close文件
//因为,文件的open和close被封装到 ReadFile 函数内部
}
4、写入操作
*** 函数说明
按照指定的模式打开文件
func OpenFile(name string, flag int, perm FileMode) (file *File, err error)
OpenFile是一个更一般性的文件打开函数,大多数调用者都应用Open或Create代替本函数。
它会使用指定的选项(如O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。
如果操作成功,返回的文件对象可用于I/O。如果出错,错误底层类型是*PathError。
第一个参数是文件路径,第二个是打开的模式,第三个在类unix系统下才有效,是权限。
常量
const (
O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
O_RDWR int = syscall.O_RDWR // 读写模式打开文件
O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件
O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用,文件必须不存在
O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O
O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件
)
*** 案例一
创建一个新文件,写入内容 5 句 “hello, Gardon”
package main
import (
"bufio"
"fmt"
"os"
)
// 创建一个文件,写入5句话
func main() {
// 指定打开方式打开文件
file, err := os.OpenFile("file.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("open file failed:", err)
return
}
// 关闭文件
defer file.Close()
// 带缓冲的写入操作(需要Flush操作)
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(fmt.Sprintf("这是第- %d -句话\n", i+1))
}
// 将缓冲区的数据刷到磁盘上
writer.Flush()
}
存在的文件覆盖
package main
import (
"bufio"
"fmt"
"os"
)
// 创建一个文件,写入5句话
func main() {
// 指定打开方式打开文件
file, err := os.OpenFile("file.txt", os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("open file failed:", err)
return
}
// 关闭文件
defer file.Close()
// 带缓冲的写入操作(需要Flush操作)
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString("新覆盖的内容\n")
}
// 将缓冲区的数据刷到磁盘上
writer.Flush()
}
*** 案例三
打开一个存在的文件,在原来的内容追加内容
package main
import (
"bufio"
"fmt"
"os"
)
// 创建一个文件,写入5句话
func main() {
// 指定打开方式打开文件
file, err := os.OpenFile("file.txt", os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Println("open file failed:", err)
return
}
// 关闭文件
defer file.Close()
// 带缓冲的写入操作(需要Flush操作)
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString("新添加的内容\n")
}
// 将缓冲区的数据刷到磁盘上
writer.Flush()
}
*** 案例四
打开一个存在的文件,将原来的内容读出显示在终端,并且追加 5 句"hello,北京!"
package main
import (
"bufio"
"fmt"
"io"
"os"
)
// 创建一个文件,写入5句话
func main() {
// 指定打开方式打开文件
file, err := os.OpenFile("file.txt", os.O_RDWR|os.O_APPEND, 0666)
if err != nil {
fmt.Println("open file failed:", err)
return
}
// 关闭文件
defer file.Close()
// 先读取文件(带缓冲的方式)
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
break
}
fmt.Printf(str)
}
// 带缓冲的写入操作(需要Flush操作)
writer := bufio.NewWriter(file)
for i := 0; i < 3; i++ {
writer.WriteString("读取之后新添加的内容...\n")
}
// 将缓冲区的数据刷到磁盘上
writer.Flush()
}
*** 文本文件拷贝
package main
import (
"fmt"
"io/ioutil"
)
// 将一个文本的数据拷贝到另一个文本
func main() {
file1Path := "file.txt"
file2Path := "file_copy.txt"
// 一次性读取,返回数据data
data, err := ioutil.ReadFile(file1Path)
if err != nil {
fmt.Printf("read file error: %v\n", err)
return
}
// 将读取到的数据data一次性写入到指定的文件路径中去
err = ioutil.WriteFile(file2Path, data, 0600)
if err != nil {
fmt.Printf("write file error: %v\n", err)
}
}
*** 拷贝jpg图片
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func CopyJPG(src string, dest string) (written int64, err error) {
// 打开文件
srcFile, err := os.Open(src)
if err != nil {
fmt.Printf("Open failed err = %v\n", err)
}
// 关闭文件
defer srcFile.Close()
// 通过srcFile,获取到Reader
reader := bufio.NewReader(srcFile)
// 打开dest
dstFile, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
fmt.Printf("Open failed err = %v\n", err)
return
}
// 关闭文件
defer dstFile.Close()
// 获取缓冲写对象
writer := bufio.NewWriter(dstFile)
// io包中的Copy函数,第一个是写入流,第二个是读取流
// 返回的是文件的大小
return io.Copy(writer, reader)
}
func main() {
srcFile := "flower.jpg"
dstFile := "flower_copy.jpg"
_, err := CopyJPG(srcFile, dstFile)
if err == nil {
fmt.Println("拷贝完成")
} else {
fmt.Printf("拷贝错误 err = %v\n", err)
}
}
5、判断文件是否存在
6、json
6.1 json基本介绍
6.2 json 数据格式说明
6.3 json 的序列化
将有 key-value 结构的数据类型(比如结构体、map、切片)序列化成 json 字符串
序列化函数Marshal
package main
import (
"encoding/json"
"fmt"
"time"
)
// 定义一个结构体
type Monster struct {
Name string
Age int
Birthday string
Salary float64
Skill string
}
// 1、序列化结构体对象
func SerializeStruct() string { // 返回序列化的结果
// 初始化一个结构体对象
monster := Monster{
Name: "kiko",
Age: 19,
Birthday: "2020-5-6 12:30:25",
Salary: 100000.0,
Skill: "金箍棒",
}
// 将结构体对象序列化
serialer, err := json.Marshal(monster)
if err != nil {
fmt.Println("Error marshallingmonster: ", err)
}
data := string(serialer)
fmt.Println(data)
return data
}
// 2、序列化map对象
func SerializeMap() string { // 返回序列化的结果
// 初始化一个map对象
var obj map[string]interface{} = make(map[string]interface{})
obj["name"] = "yoyo"
obj["age"] = 20
obj["gender"] = "male"
obj["birthday"] = time.Now()
// 序列化map对象
serialer, err := json.Marshal(obj)
if err != nil {
fmt.Println("serialize map error: ", err)
}
data := string(serialer)
fmt.Println(data)
return data
}
// 3、序列化切片
func SerializeSlice() string {
// 初始化切片数据
var slice []map[string]interface{}
m1 := make(map[string]interface{})
m1["name"] = "kiko"
m1["age"] = 20
m1["gender"] = "male"
m1["birthday"] = time.Now()
m1["address"] = [2]string{"夏威夷", "三亚"}
slice = append(slice, m1)
m2 := make(map[string]interface{})
m2["name"] = "yoyo"
m2["age"] = 21
m2["gender"] = "woman"
m2["birthday"] = time.Now()
m2["address"] = [2]string{"青岛", "香格里拉"}
slice = append(slice, m2)
// 序列化切片数据
serialer, err := json.Marshal(slice)
if err != nil {
fmt.Println("Error marshallingmonster err : ")
}
data := string(serialer)
fmt.Println(data)
return data
}
// 4、序列化Float类型
func SerializeFloat() string {
// 序列化一个数字
var f float64 = 12.34
serialer, err := json.Marshal(f)
if err != nil {
fmt.Println("Error marshalling err: ", err)
}
data := string(serialer)
fmt.Println(data)
return data
}
func main() {
SerializeStruct()
SerializeMap()
SerializeSlice()
SerializeFloat()
}
注意事项
6.4 json的反序列化
反序列化函数
基本介绍
// 1、反序列化操作 --> 结构体
func UnSerializeStruct() {
// 得到json串
srcJson := SerializeStruct()
// 反序列化
var monster Monster
// 第二个参数必须传地址,结构体是值类型的,否则无法获取数据
err := json.Unmarshal([]byte(srcJson), &monster)
if err != nil {
return
}
fmt.Println("Monster:", monster)
}
// 2、反序列化操作 --> map
func UnSerializeMap() {
// 得到json串
srcJson := SerializeMap()
// 反序列化
var obj map[string]interface{}
// 反序列化 map,不需要 make,因为 make 操作被封装到 Unmarshal 函数
err := json.Unmarshal([]byte(srcJson), &obj)
if err != nil {
return
}
fmt.Println(obj)
}
// 3、反序列化操作 --> slice
func UnSerializeSlice() {
// 得到json串
srcJson := SerializeSlice()
// 反序列化
var obj []map[string]interface{}
// 反序列化 map,不需要 make,因为 make 操作被封装到 Unmarshal 函数
err := json.Unmarshal([]byte(srcJson), &obj)
if err != nil {
return
}
fmt.Println(obj)
}
Go单元测试
1、需求引入
在工作中,我们会遇到这样的情况,就是去确认一个函数,或者一个模块的结果是否正确。
2、传统测试方式
*** 传统的方式来进行测试
在 main 函数中,调用 addUpper 函数,看看实际输出的结果是否和预期的结果一致,如果一致,则说明函数正确,否则函数有错误,然后修改错误。
*** 传统方法的缺点分析
- 不方便, 我们需要在 main 函数中去调用,这样就需要去修改 main 函数,如果现在项目正在运行,就可能去停止项目。
- 不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在 main 函数,不利于我们管理和清晰我们思路。
- 引出单元测试。-> testing 测试框架 可以很好解决问题。
3、单元测试-基本介绍
轻量级的测试框架 testing 和自带的 go test 命令
- 确保每个函数是可运行,并且运行结果是正确的。
- 确保写出来的代码性能是好的。
- 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定。
4、单元测试-案例
5、总结
文件名必须以 _test.go 结尾用例函数必须以 Test 开头且Test之后的第一个字母必须大写go testgo test -v使用 t.Fatalf 来格式化输出错误信息t.Logf要带上被测试的原文件如果使用go test -v命令,那么默认执行包中的所有文件中的测试用例go test -v xxx_test.go 被测试的函数所在文件
go test -v -test.run 测试函数名
// 如果上面的有问题,可以写下面的
go test -v -run 测试函数名 测试函数所在文件
// go test -v -ru TestXxx main_test.go
goroutine 和 channel
1、需求引入
要求统计 1-9000000000 的数字中,哪些是素数?
分析思路:
- 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。[很慢]
- 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到goroutine.【速度提高 4 倍】
2、相关概念介绍
进程和线程程序、进程和线程的关系并发和并行
- 多线程程序在单核上运行,就是并发
- 多线程程序在多核上运行,就是并行
Go 协程和 Go 主线程
特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
3、快速入门
package main
import (
"fmt"
"time"
)
func print() {
for i := 0; i < 100; i++ {
fmt.Printf("Hello Print %d\n", i)
time.Sleep(time.Second)
}
}
func main() {
go print() // 开启协程
for i := 0; i < 10; i++ {
fmt.Printf("Hello Main %d\n", i)
time.Sleep(time.Second)
}
}
小结:
可以轻松的开启上万个协程
4、设置 Golang 运行的 cpu 数
为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目。
num := runtime.NumCPU() // 获取逻辑CPU的个数
runtime.GOMAXPROCS(num - 3) // 设置运行CPU的个数
5、协程并发(并行)资源竞争
package main
import (
"fmt"
"time"
)
// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。
// 最后显示出来。要求使用 goroutine 完成
// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map 中.
// 2. 我们启动的协程多个,统计的将结果放入到 map 中
// 3. map 应该做出一个全局的.
var (
myMap = make(map[int]int, 10)
)
// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//这里我们将 res 放入到 myMap
myMap[n] = res //concurrent map writes?
}
func main() {
// 我们这里开启多个协程完成这个任务[200 个]
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠 10 秒钟【第二个问题 】
time.Sleep(time.Second * 10)
//这里我们输出结果,变量这个结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
fatal error: concurrent map writes
6、使用全局变量加锁同步改进程序
package main
import (
"fmt"
"sync"
"time"
)
var (
myMap = make(map[int]int, 10)
// 定义一个互斥锁
locker sync.Mutex
)
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
// 锁住全局资源的写操作
locker.Lock()
myMap[n] = res
locker.Unlock()
}
func main() {
for i := 1; i <= 20; i++ {
go test(i)
}
//休眠 5 秒钟
time.Sleep(time.Second * 5)
//这里我们输出结果,变量这个结果
for i, v := range myMap {
fmt.Printf("map[%d] = %d\n", i, v)
}
}
总结:
- 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
- 主线程在等待所有 goroutine 全部完成的时间很难确定,上面设置的5秒, 10 秒,仅仅是估算。
- 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁
- 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
- 为此,channel通信机制应运而生。
7、channe管道
7.1 基本介绍
- channle 本质就是一个数据结构-队列。
- 数据是先进先出【FIFO : first in first out】。
- 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的。
- channel是 有类型的,一个 string 的 channel 只能存放 string 类型数据。
7.2 管道的声明/定义
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
7.3 管道的初始化,写入,读取及注意事项
package main
import (
"fmt"
)
func main() {
// 1、创建一个可以存放3个string类型的管道
var strChan chan string
strChan = make(chan string, 3) // 容量是3
// 2、查看strChan是什么
fmt.Printf("strChan 的值 = %v strChan 本身的地址= %p\n", strChan, &strChan)
// 3、向管道写入数据。
// 需要注意的是,与map切片不同的是,channel创建时指定的容量无法更改,他不能自动扩容,故而写入的数据不能超过容量
strChan <- "Java"
strChan <- "Python"
str := "Golang"
strChan <- str
// strChan <- "str" // fatal error: all goroutines are asleep - deadlock!(超出了容量报错)
// 4、查看管道的长度和容量
fmt.Println("channel len: ", len(strChan), " channel cap: ", cap(strChan))
// 5、从管道中读取数据(读取数据也相当于把数据从管道中拿了出来,管道的长度也就减少了)
var retStr string
retStr = <-strChan
fmt.Println("retStr: ", retStr)
fmt.Println("channel len: ", len(strChan), " channel cap: ", cap(strChan))
// 6、在没有使用协程的情况下,如果管道中的数据已经取完了,再取就会报错,和写入是一样的,都不能过度
retStr1 := <-strChan
retStr2 := <-strChan
// retStr3 := <-strChan // fatal error: all goroutines are asleep - deadlock!
fmt.Println(retStr1, retStr2)
}
基本注意事项
- channel 中只能存放指定的数据类型。
- channle 的数据放满后,就不能再放入了。
- 如果从 channel 取出数据后,可以继续放入。
- 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock。
7.4 channel案例
1、创建一个intChan,最多可以存放3个int,演示存3个数据到intChan,然后再取出。
2、创建一个mapChan,最多可以存放10个map[string]string的key-val,演示读取和写入。
3、创建一个catChan,最多可以存放10个Cat结构体变量,演示读取和写入的用法。
*Cat
5、创建一个allChan,最多可以存放10个任意数据类型变量,演示读取和写入。
7.5 channel 遍历和关闭
channel关闭内置函数 close不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据
遍历
channel 支持 for–range 的方式进行遍历,需要注意两个细节:
如果 channel 没有关闭,则会出现 deadlock 的错误
package main
import (
"fmt"
)
func main() {
// 初始化管道数据
intChan := make(chan int, 15)
for i := 0; i < 15; i++ {
intChan <- i * 2 // 写入数据
}
// 遍历管道不能使用普通的for循环
// for i := 0; i < len(intChan); i++ {
// }
// 在遍历时,如果channel没有关闭,则会出现deadlock的错误
// 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
close(intChan) // 如果不关闭,就会报 fatal error: all goroutines are asleep - deadlock!
for val := range intChan {
fmt.Printf("%v\t", val)
}
}
7.6 管道与协程应用
实例1
package main
import (
"fmt"
)
// 写数据的协程函数
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan <- i
}
// 写完数据就关闭
close(intChan)
}
// 读数据的协程函数
func readData(intChan chan int, exitChan chan bool) {
for {
val, ok := <-intChan
if !ok {
break
}
fmt.Printf("%v\t", val)
}
// 读完数据要往exitChan中写入true
exitChan <- true
close(exitChan)
}
func main() {
// 创建两个管道
var intChan chan int = make(chan int, 50)
var exitChan chan bool = make(chan bool, 1)
// 开启协程
go writeData(intChan)
go readData(intChan, exitChan)
// 阻塞读,防止主线程关闭导致协程未执行完就被终止
<-exitChan
}
*** 练习
程序中做了改写,协程数量和数做了扩大。
package main
import (
"fmt"
"sync"
)
const (
// 常量,表示协程的个数
NUMROUNTINE = 10000
)
var (
flag = NUMROUNTINE //标志协程个数,每一个协程都要经历退出循环操作
locker sync.Mutex
)
// 协程函数,用于向numchan管道中写入数据
func writeNums(numchan chan int) {
for i := 1; i <= cap(numchan); i++ {
numchan <- i
}
// 写完则关闭管道
close(numchan)
}
// 协程函数,供8个协程调用,读取numchan管道数据并且计算写入reschan管道
func getNums(numchan chan int, reschan chan map[int]int) {
// 从numchan中读取数据
for {
val, ok := <-numchan
if !ok { // 如果ok等于false,说明numchan被关闭了,且当前已经读完
locker.Lock()
flag-- // 这个是共享资源,必须要锁住
if flag == 0 {
close(reschan)
}
locker.Unlock()
break
}
// 计算结果
m := make(map[int]int, 1)
m[val] = getSum(val)
reschan <- m
}
}
func getSum(n int) int {
res := 0
for i := 1; i <= n; i++ {
res += i
}
return res
}
func main() {
// 创建两个管道
var numchan chan int = make(chan int, 80000)
var reschan chan map[int]int = make(chan map[int]int, 80000)
go writeNums(numchan)
for i := 0; i < NUMROUNTINE; i++ {
go getNums(numchan, reschan)
}
for {
val, ok := <-reschan
if ok {
fmt.Printf("%v\n", val)
} else {
break
}
}
}
实例2
实例3
package main
import (
"fmt"
"time"
)
func putData(intChan chan int) {
for i := 1; i <= 800000; i++ {
intChan <- i
}
close(intChan)
}
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
num, ok := <-intChan
if !ok {
break
}
flag = true //假设是素数
for i := 2; i < num; i++ {
if num % i == 0 {
flag = false // 不是素数
break
}
}
if flag {
// 如果是素数就将这个数放到primeChan中
primeChan <- num
}
}
fmt.Println("有一个primeNum协程因为取不到数据退出...")
// 这里还不能关闭
// 向exitChan写入true
exitChan <- true
}
func main() {
t1 := time.Now()
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
// 标识退出的管道
exitChan := make(chan bool, 4)
// 将管道中的数据存放到切片中
var data []int
// 开启一个协程,向intChan放入1-8000个数据
go putData(intChan)
// 开启四个协程,从intChan中取出数据判断是否是素数,并且放到primeChan中
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
// 开启一条协程负责去关闭primeChan协程
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
// 当我们从exitChan中取出4个结果,就可以关闭primeChan
close(primeChan)
}()
// 遍历primeChan,把结果取出来
for {
val, ok := <-primeChan
if !ok {
break
}
// 把结果输出
// fmt.Printf("素数 = %d\n", val)
data = append(data, val)
}
t2 := time.Now()
fmt.Println("len data: ", len(data))
fmt.Println(t2.Second() - t1.Second())
}
7.7 channel 使用细节和注意事项
1、管道可以设置为只可读或者只可写。
默认情况下,声明一个管道是可读可写的。
可以将一个可读可写的管道传参给一个只可读或者只可写的管道,所以上面的素数判断可以根据实际情况改写。
使用只读只写信道再次改进判断素数,并且和普通的方式对比。
package main
import (
"fmt"
"time"
)
const (
// 开启的协程数量
NUMCPU = 4
)
// 往信道中写入数据
func PutData(intChan chan<- int) {
// 需要判断的数据, 1- 800000, 往信道中写入数据
for i := 1; i <= 800000; i++ {
intChan <- i
}
// 数据写入完毕,关闭信道,让读取的该信道的协程能够判断出来终止读操作
close(intChan)
}
// 读取intChan信道
// 从intChan读取数据,然后判断是否是素数,如果是就存入primeChan中,intChan读取完之后就往exitChan中写入数据
func TakeData(intChan <-chan int, primeChan chan<- int, exitChan chan<- bool) {
for {
val, ok := <-intChan
if !ok {
// 如果读取完毕就跳出循环
break
}
flag := JustPrime(val) // 判断是否为素数
if flag {
// 如果是素数就往primeChan中添加
primeChan <- val
}
}
// 当读取intChan信道完毕,说明没有数据需要判断了
exitChan <- true
}
// 判断num是否为素数
func JustPrime(num int) bool {
flag := true
for i := 2; i < num; i++ {
if num%i == 0 { // 说明该 num 不是素数
flag = false
break
}
}
return flag
}
func main() {
// 创建3个信道
intChan := make(chan int, 100000)
primeChan := make(chan int, 200000)
exitChan := make(chan bool, NUMCPU) // exitChan只写NUMCPU个,这里要开NUMCPU个协程
start := time.Now()
go PutData(intChan)
for i := 0; i < NUMCPU; i++ {
// 开启NUMCPU个协程判断素数
go TakeData(intChan, primeChan, exitChan)
}
// 这里开启一个协程,用于判断程序是否结束
go func() {
for i := 0; i < NUMCPU; i++ {
<-exitChan // 当遍历出了四个就说明程序结束了
}
// 当exitChan读取到了NUMCPU个,就说明primeChan需要close
close(primeChan)
}()
// 读取PrimeChan到切片中
var retData []int
for {
val, ok := <-primeChan
if !ok {
break // 结束
}
// 将素数存放到切片中
retData = append(retData, val)
}
end := time.Now()
fmt.Printf("花费了 %d 毫秒\n", end.UnixMilli()-start.UnixMilli())
fmt.Printf("---有--> %d <--个素数\n", len(retData))
}
2、使用 select 可以解决从管道取数据的阻塞问题
package main
import (
"fmt"
"time"
)
func main() {
// 使用select 可以解决从管道取数据的阻塞问题
// 1、定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
// 2、定义一个管道 5个数据string
strChan := make(chan string, 5)
for i := 0; i < 5; i++ {
strChan <- "Hello," + fmt.Sprintf(" %d", i)
}
// 传统的方法在遍历信道时,如果不关闭读取会阻塞而导致 deadLock
// 在实际开发中,多协程操作一个管道的时候,不好确定什么时候关闭管道
// 使用select可以解决这个问题
for {
select {
case v := <-intChan:
fmt.Println("intChan读取的数据--> ", v)
time.Sleep(time.Second * 2)
case w := <-strChan:
fmt.Println("strChan读取到的数据--> ", w)
time.Sleep(time.Second * 2)
default:
fmt.Println("读取不到数据")
return
}
}
}
3、goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
package main
import (
"fmt"
"time"
)
// 函数
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello, world")
}
}
// 函数
func test() {
//这里我们可以使用 defer + recover
defer func() {
//捕获 test 抛出的 panic
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
//定义了一个 map
var myMap map[int]string
myMap[0] = "golang" //error, map还没有被make
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}
Go的TCP编程
1、通信案例引入
写一个客户端和服务端,客户端循环发送数据,服务端接收然后输出这个数据。
服务端
package main
import (
"fmt"
_ "io"
"net"
)
func Process(conn net.Conn) {
// 循环接收客户端发送的数据
defer conn.Close()
for {
buf := make([]byte, 1024)
// 等待客户端通过conn发送数据,如果客户端没有发送(write),就会阻塞在此
fmt.Printf("服务器在等待客户端 %s 的输入...... ", conn.RemoteAddr().String())
n, err := conn.Read(buf) // 从conn读取
if err != nil {
fmt.Println("read error: ", err)
return
}
// 显示接收到的数据
fmt.Print(string(buf[:n]))
}
}
func main() {
fmt.Println("服务器开始监听...")
// tcp 表示使用网络协议tcp
// 0.0.0.0:8888 表示在本地监听 8888端口
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("ERROR: ", err)
return
}
defer listen.Close()
// 循环等待客户端连接
for {
// 等待客户端连接
fmt.Println("等待客户端连接......")
conn, err := listen.Accept() // 阻塞等待
if err != nil {
fmt.Println("ERROR: ", err)
continue
} else {
fmt.Printf("Accept successful: 客户端是 = %v\n", conn.RemoteAddr().String())
}
// 开启一个协程,为一个客户端服务
go Process(conn)
}
// listen: *net.TCPListener, &{0xc0000cca00 {<nil> 0}}
// fmt.Printf("listen: %T, %v", listen, listen)
}
客户端
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "192.168.1.13:8888")
if err != nil {
fmt.Println("Error connecting Error: ", err)
return
}
reader := bufio.NewReader(os.Stdin) // 从标准输入中创建一个缓冲读
for {
// 从终端读取一行用户输入,并发送给服务器
content, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Error reading content: ", err)
return
}
content = strings.Trim(content, "\r\n")
if content == "exit" {
fmt.Println("客户端退出...")
break
}
// 将content发送给服务器
_, err = conn.Write([]byte(content + "\n"))
if err != nil {
fmt.Println("Error writing content: ", err)
return
}
}
}
2、API介绍
根据上面的案例,总结使用的API
服务端
监听
连接
Accept()(c Conn, err error)Conn对象
一个客户端发送连接服务器端就会生成一个新的Conn对象,这个Conn对象负责管理服务器和请求的客户端的连接。通信时都需要这个对象。
服务端并发时,通过协程处理,将Conn对象传入协程执行的函数。
客户端
Conn对象
获取Conn对象之后,就可以使用Conn的方法,同服务端的Conn的函数一样。