Go面向对象编程

1、Golang 语言面向对象编程说明

  1. Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
  2. Golang 没有类(class)Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,可以理解 Golang 是基于 struct 来实现 OOP 特性的。
  3. Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
  4. Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有extends 关键字继承是通过匿名字段来实现
  5. Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。也就是说在 Golang 中面向接口编程是非常重要的特性。

2、结构体

2.1、声明结构体

// 基本语法
type 结构体名称 struct {field1 typefield2 type
}
//举例:
type Student struct {Name string //字段Age int //字段Score float32
}

2.2 字段 / 属性

*** 基本介绍

  1. 从概念或叫法上看: 结构体字段 = 属性 = field
  2. 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体 的 Name string 就是属性。

*** 注意事项和说明

  1. 字段声明语法同变量,示例:字段名 字段类型

  2. 字段的类型可以为:基本类型、数组或引用类型

  3. 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面的一样:
    布尔类型是 false ,数值是 0 ,字符串是 ""
    数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]
    指针,slice,和 map 的零值都是 nil ,即还没有分配空间

  4. 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型

package mainimport ("fmt"
)type NilChecker struct {ptr   *intslice []stringmmp   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、mapfmt.Printf("mmp地址 %p\t", check.mmp)fmt.Printf("mmp地址 %t\n", check.mmp == nil)
}

package mainimport ("fmt"
)type Stu struct {Name stringage  int
}func main() {// 测试结构体是值类型var sst Stusst.Name = "kiko"sst.age = 12fmt.Println("sst : ", sst)sst_1 := sstsst_1.Name = "yoyo"sst_1.age = 15fmt.Println("sst_1 : ", sst_1)fmt.Println("sst : ", sst)
}

2.2 创建结构体变量和访问结构体字段

*** 方式1 直接声明

 var person Person

*** 方式2

 var person Person = Person{} // 如果需要,可以在{}给字段赋值
package mainimport ("fmt"
)type Stu struct {Name stringage  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 mainimport ("fmt"
)type Stu struct {Name stringage  int
}func main() {// 创建指针var st *Stu = new(Stu)(*st).Name = "yoyo"(*st).age = 19fmt.Println(st)st.Name = "kiko" // 可以解引用,go设计者做了处理st.age = 18fmt.Println(st)
}

*** 方式4 &{}

// 结合了第二种方式和第三种方式

 var person *Person = &Person{} // {}和上面一样可以给字段赋值

*** 四种方式说明

  1. 第 3 种和第 4 种方式返回的是 结构体指针。
  2. 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名 ,比如 (*person).Name = “tom”
  3. go 做了一个简化,也支持 结构体指针.字段名, 比如 person.Name = “tom”。更加符合程序员使用的习惯,go 编译器底层 对 person.Name 做了转化 (*person).Name。
  4. go中.运算符比*运算符优先级高,所以不能写成 *p.Name

2.3 结构体使用注意事项和细节

  1. 结构体的所有字段在内存中是连续的
package mainimport ("fmt"
)type Point struct {x int32y 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))
}

  1. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
  2. 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转。
  3. struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
    结构体中的字段名如果不大写开头,那么就不能在别的包中使用,但是如果大写了,面对一些例如序列化成json串的时候,它的键的首字母就会是大写,因此出现tag标签功能
package mainimport ("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 基本介绍

Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct。
方法往往是提供一系列的行为。

3.2 方法快速入门

package mainimport ("fmt"
)type Person struct {name stringage  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 := 0for 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)
}

说明

  1. func (person Person) test() {} 表示 Person 结构体有一方法,方法名为 test。
  2. (person Person) 体现 test 方法是和 Person 类型绑定的。
  3. 方法只能通过绑定的对象来调用,不能和函数一样直接被调用。
  4. func (p Person) test() { }… p 表示哪个 Person 变量调用,这个 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 方法的注意事项和细节

  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
  2. 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理(常用这种方式绑定,内存消耗也要小很多)。
  3. Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法。
type Integer int  // 需要重新定义类型,变为自定义类型func (v *Integer) changeValue() {(*v) = 999
}func main() {var v Integer = 100(&v).changeValue()fmt.Println(v)  // 999
}
  1. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问

  2. 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出

package mainimport ("fmt"
)type Person struct {name stringage  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 方法和函数的区别

  1. 调用方式不一样
函数的调用方式: 函数名(实参列表)
方法的调用方式: 变量.方法名(实参列表)
  1. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。
  2. 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。
  3. 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定。
  4. 如果是和值类型,比如 (p Person) , 则是值拷贝, 如果和指针类型,比如是 (p *Person) 则是地址拷贝。

4、工厂模式

Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题

需求引入

如果在另外一个包中定义了一个结构体,如果结构体的名称的首字母是大写,那么在其他包中可以顺利的创建对象;如果首字母是小写,那么就无法在其他包中创建对象,为了解决这个问题,使用工厂模式的方式。


此外,如果结构体中的字段的名称的首字母是小写的,其他包肯定也无法访问,为了解决这个问题,可以通过方法来解决。代码修改如下:

package modeltype student struct {name stringage  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、面向对象三大特性之封装

*** 基本介绍
Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样。
*** 封装介绍
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。

*** 封装的理解和好处

  1. 隐藏实现细节
  2. 可以对数据进行验证,保证安全合理(比如要求设置的Age要大于0小于125)

*** 如何体现封装

  1. 对结构体中的属性进行封装
  2. 通过方法,包 实现封装

*** 封装的实现步骤

  1. 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
  2. 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
  3. 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
func (varia 结构体类型名) SetXxx(参数列表) (返回值列表) {//加入数据验证的业务逻辑varia .字段 = 参数
}
  1. 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
func (varia 结构体类型名) GetXxx() {return varia.age;
}
package modelimport "fmt"type person struct {Name   stringage    int // 其他包不能直接访问age和salarysalary 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 mainimport ("fmt"
)// 定义Student结构体和相关方法
type Student struct {name  stringage   intscore 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 = 8puil.testing()puil.Student.SetScore(90)puil.Student.ShowInfo()fmt.Println("----------------------------------------------------")// 大学生graduate := &Graduate{}graduate.Student.name = "yoyo"graduate.Student.age = 18graduate.testing()graduate.Student.SetScore(130)graduate.Student.ShowInfo()
}

*** 继承深入探讨

  1. 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。
    当然上面的说法仅限于两个结构体在同一个包中,如果嵌套的匿名结构体在另外一个包中,那么就必须是大写字母开头才可以访问。
  2. 匿名结构体字段访问可以简化
type A struct {Name stringage  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 Bb.A.Name = "kiko"b.A.age = 10b.A.SayOk()b.A.SayHello()// 上面的写法可以简化b.Name = "yoyo"b.age = 20b.SayOk()b.SayHello()
}

对上面的代码小结
(1) 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
(2) 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段
(3) 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有继续查找…如果都找不到就报错。

  1. 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如果希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分。

  2. 结构体嵌入两个(或多个)匿名结构体(多重继承,不建议使用),如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。

type A struct {Name stringage  int
}type B struct {Name  stringscore int
}type C struct {AB// C结构体本身没有Name字段
}func main() {var c C// 如果C结构体本身有Name字段,那么c.Name是正确的,给C本身的额赋值// c.Name = "kiko"  // 错误,指向不明确c.A.Name = "yoyo"    // 正确}
  1. 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字。这种模式就不是继承关系,只能当做一个成员,按照普通的成员使用即可。

  1. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值。
type Goods struct {Name  stringPrice float32
}type Brand struct {Name    stringAddress string
}type TV struct {GoodsBrand
}// 也可以使用这种地址方式的匿名结构体
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、接口(面向接口编程)

在 Golang 中 多态特性主要是通过接口来体现

7.1 接口入门案例

计算机有很多USB接口,不同的设备插入这个USB借口,计算机能够识别且执行不同的功能。

package mainimport ("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 接口概念

interface 类型可以定义一组方法,但是这些不需要实现,也不能在内部实现。并且 interface 不能包含任何变量。到某个自定义类型(比如结构体 Mouse)要使用的时候,再根据具体情况把这些方法写出来(实现)。

说明:

  1. 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
  2. Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字

7.3 注意事项和细节

  1. 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)

  2. 接口中所有的方法都没有方法体,即都是没有实现的方法。

  3. 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口

  4. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型。

  5. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。

  6. 一个自定义类型可以实现多个接口

  7. Golang 接口中不能有任何变量。

  8. 一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法也全部实现。
    接口继承其他接口和结构体继承一样,通过匿名接口实现。

  9. interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil。此外,因为接口是引用类型,当将实现了接口的结构体赋值给接口变量时,结构体创建的实例必须是返回引用的方式

  10. 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口。

package mainimport ("fmt"
)// 空接口就是没有任何的方法声明的一个接口,
// 任何类型的变量都可以赋值给空接口类型的变量
type Mouse struct {}// 定义一个空接口
type T interface {}func main() {// 1、使用已经定义好的接口类型Tvar mouse T = Mouse{}fmt.Println(mouse)var i int = 10mouse = ifmt.Println(mouse)// 2、直接定义空接口var E interface{} = mousefmt.Println(E)}

  1. 两种特殊的情况
package mainimport ("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 mainimport ("fmt"
)/*
2、情况二
有三个接口ABC,其中AB接口有两个方法,其中一个是方法名相同的
接口C继承了AB
*/
type AInterface interface {SayHello()SayOk1()
}
type BInterface interface {SayHello()SayOk2()
}type CInterface interface {AInterfaceBInterface
}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 mainimport ("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()
}


代码小结:

  1. 当 A 结构体继承了 B 结构体,那么 A 结构就自动的继承了 B 结构体的字段和方法,并且可以直接使用
  2. 当 A 结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充

*** 接口和继承解决的解决的问题不同
继承的价值主要在于:解决代码的复用性和可维护性
接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。

  1. 接口比继承更加灵活 Person Student BirdAble LittleMonkey
  2. 接口比继承更加灵活,继承是满足 is-a 的关系,而接口只需满足 like-a 的关系
  3. 接口在一定程度上实现代码解耦。

7.5 接口编程的最佳实践

排序:包"sort"中有一个函数func Sort(data Interface),这个函数可以对实现了Interface接口对象进行排序。Interface接口声明如下:

type Interface interface {// Len方法返回集合中的元素个数Len() int// Less方法报告索引i的元素是否比索引j的元素小Less(i, j int) bool// Swap方法交换索引i和j的两个元素Swap(i, j int)
}

*** 切片方式

package mainimport ("fmt""math/rand""sort"
)// 1、声明Hero结构体
type Hero struct {name stringage  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 HeroSlicefor 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 mainimport ("fmt""math/rand""sort"
)// 1、声明Hero结构体
type Hero struct {name stringage  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 HeroArrayfor 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 mainimport ("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]USBusbArr[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 stringage  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、文件的基本介绍

*** 文件概念
文件,对我们并不陌生,文件是数据源(保存数据的地方)的一种,比如大家经常使用的 word 文档,txt 文件,excel 文件…都是文件。文件最主要的作用就是保存数据,它既可以保存一张图片,也可以保持视频,声音…

*** 输入流和输出流

*** os.File
os.File 封装所有文件相关操作,File 是一个结构体。对于文件操作的学习主要是针对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 mainimport ("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 mainimport ("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 一次将整个文件读入到内存中),这种方式适用于文件不大的情况。相关方法和函数(ioutil.ReadFile)

func ReadFile(filename string) ([]byte, error)ReadFile 从filename指定的文件中读取数据并返回文件的内容。
成功的调用返回的err为nil而非EOF。
因为本函数定义为读取整个文件,它不会将读取返回的EOF视为应报告的错误。
package mainimport ("fmt""io/ioutil"
)func main() {// 使用ioutil.ReadFile一次性将文件读取到位file := "G:/Golang/GoProjects/files/test.txt"content, err := ioutil.ReadFile(file) // 返回的是[]byteif 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/OO_TRUNC  int = syscall.O_TRUNC  // 如果可能,打开时清空文件
)

*** 案例一
创建一个新文件,写入内容 5 句 “hello, Gardon”

package mainimport ("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()
}

*** 案例二
打开一个存在的文件中,将原来的内容覆盖成新的内容 10 句 “你好,尚硅谷!”

package mainimport ("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 mainimport ("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 mainimport ("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 mainimport ("fmt""io/ioutil"
)// 将一个文本的数据拷贝到另一个文本
func main() {file1Path := "file.txt"file2Path := "file_copy.txt"// 一次性读取,返回数据datadata, 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 mainimport ("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,获取到Readerreader := bufio.NewReader(srcFile)// 打开destdstFile, 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 的序列化

*** 基本介绍
json 序列化是指,将有 key-value 结构的数据类型(比如结构体、map、切片)序列化成 json 字符串的操作。

*** 序列化函数
在json包中有一个函数Marshal

package mainimport ("encoding/json""fmt""time"
)// 定义一个结构体
type Monster struct {Name     stringAge      intBirthday stringSalary   float64Skill    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"] = 20obj["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"] = 20m1["gender"] = "male"m1["birthday"] = time.Now()m1["address"] = [2]string{"夏威夷", "三亚"}slice = append(slice, m1)m2 := make(map[string]interface{})m2["name"] = "yoyo"m2["age"] = 21m2["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.34serialer, 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()}

*** 注意事项
对于结构体的序列化,如果我们希望序列化后的 key 的名字,又我们自己重新制定,那么可以给 struct指定一个 tag 标签。

6.4 json的反序列化

*** 反序列化函数

*** 基本介绍
json反序列化是指,将 json 字符串反序列化成对应的数据类型(比如结构体、map、切片)的操作。

// 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 函数,看看实际输出的结果是否和预期的结果一致,如果一致,则说明函数正确,否则函数有错误,然后修改错误。

*** 传统方法的缺点分析

  1. 不方便, 我们需要在 main 函数中去调用,这样就需要去修改 main 函数,如果现在项目正在运行,就可能去停止项目。
  2. 不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在 main 函数,不利于我们管理和清晰我们思路。
  3. 引出单元测试。-> testing 测试框架 可以很好解决问题。

3、单元测试-基本介绍

Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试,testing 框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以解决如下问题:

  1. 确保每个函数是可运行,并且运行结果是正确的。
  2. 确保写出来的代码性能是好的。
  3. 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定。

4、单元测试-案例

5、总结

  1. 测试用例文件名必须以 _test.go 结尾。 比如上面的main_test.go 。

  2. 测试用例函数必须以 Test 开头且Test之后的第一个字母必须大写,一般来说就是 Test+被测试的函数名,比如 TestAddUpper。

  3. TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.T类型。

  4. 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSubLower。

  5. 运行测试用例指令
    (1) cmd>go test [如果运行正确,无日志,错误时,会输出日志]
    (2) cmd>go test -v [运行正确或是错误,都输出日志]

  6. 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序

  7. t.Logf 方法可以输出相应的日志

  8. PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败

  9. 测试单个文件,一定要带上被测试的原文件
    在上面代码的基础上,增加除法函数并且增加新的文件用于测试。如果使用go test -v命令,那么默认执行包中的所有文件中的测试用例
    如果想要测试其中一个测试文件中的测试用例,可以使用命令go test -v xxx_test.go 被测试的函数所在文件

  10. 测试单个方法

go test -v -test.run 测试函数名
// 如果上面的有问题,可以写下面的
go test -v -run 测试函数名 测试函数所在文件
// go test -v -ru TestXxx main_test.go

goroutine 和 channel

1、需求引入

要求统计 1-9000000000 的数字中,哪些是素数?
分析思路:

  1. 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。[很慢]
  2. 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到goroutine.【速度提高 4 倍】

2、相关概念介绍

*** 进程和线程

*** 程序、进程和线程的关系示意图

*** 并发和并行

  1. 多线程程序在单核上运行,就是并发
  2. 多线程程序在多核上运行,就是并行

*** Go 协程和 Go 主线程
Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,可以这样理解,协程是轻量级的线程[编译器做优化]。

Go 协程的特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 协程是轻量级的线程

3、快速入门

package mainimport ("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)}
}


小结:

  1. 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  3. Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了。

4、设置 Golang 运行的 cpu 数

为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目。

num := runtime.NumCPU()     // 获取逻辑CPU的个数
runtime.GOMAXPROCS(num - 3) // 设置运行CPU的个数

5、协程并发(并行)资源竞争

package mainimport ("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 := 1for i := 1; i <= n; i++ {res *= i}//这里我们将 res 放入到 myMapmyMap[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)}
}

代码说明:
代码中有两个比较严重的问题;
第一,首要问题,多个协程并发执行求阶乘将结果存入全局资源map中,这种情况下会出现fatal error: concurrent map writes错误。
第二,主线程最后要遍历输出全局map,但是此时的求阶乘的所有协程基本上都没有执行完毕,为了解决这个问题,上面使用了睡眠10秒的方式,但是这种处理方式并不合理。

6、使用全局变量加锁同步改进程序

package mainimport ("fmt""sync""time"
)var (myMap = make(map[int]int, 10)// 定义一个互斥锁locker sync.Mutex
)func test(n int) {res := 1for i := 1; i <= n; i++ {res *= i}// 锁住全局资源的写操作locker.Lock()myMap[n] = reslocker.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)}
}


总结:

  1. 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
  2. 主线程在等待所有 goroutine 全部完成的时间很难确定,上面设置的5秒, 10 秒,仅仅是估算。
  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁
  4. 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
  5. 为此,channel通信机制应运而生。

7、channe管道

7.1 基本介绍

  1. channle 本质就是一个数据结构-队列。
  2. 数据是先进先出【FIFO : first in first out】。
  3. 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的。
  4. 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 mainimport ("fmt"
)func main() {// 1、创建一个可以存放3个string类型的管道var strChan chan stringstrChan = 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 stringretStr = <-strChanfmt.Println("retStr: ", retStr)fmt.Println("channel len: ", len(strChan), " channel cap: ", cap(strChan))// 6、在没有使用协程的情况下,如果管道中的数据已经取完了,再取就会报错,和写入是一样的,都不能过度retStr1 := <-strChanretStr2 := <-strChan// retStr3 := <-strChan  // fatal error: all goroutines are asleep - deadlock!fmt.Println(retStr1, retStr2)
}

*** 基本注意事项

  1. channel 中只能存放指定的数据类型。
  2. channle 的数据放满后,就不能再放入了。
  3. 如果从 channel 取出数据后,可以继续放入。
  4. 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock。

7.4 channel案例

1、创建一个intChan,最多可以存放3个int,演示存3个数据到intChan,然后再取出。

2、创建一个mapChan,最多可以存放10个map[string]string的key-val,演示读取和写入。

3、创建一个catChan,最多可以存放10个Cat结构体变量,演示读取和写入的用法。

4、创建一个catChan2,最多可以存放10个*Cat变量,演示读取和写入。

5、创建一个allChan,最多可以存放10个任意数据类型变量,演示读取和写入。

7.5 channel 遍历和关闭

*** channel关闭
使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据

*** 遍历

channel 支持 for–range 的方式进行遍历,需要注意两个细节:

  1. 在遍历时,如果 channel 没有关闭,则会出现 deadlock 的错误
  2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
package mainimport ("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 mainimport ("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 := <-intChanif !ok {break}fmt.Printf("%v\t", val)}// 读完数据要往exitChan中写入trueexitChan <- trueclose(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 mainimport ("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 := <-numchanif !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 := 0for 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 := <-reschanif ok {fmt.Printf("%v\n", val)} else {break}}
}

*** 实例2

*** 实例3
解决一开始的素数问题

package mainimport ("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 boolfor {num, ok := <-intChanif !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写入trueexitChan <- 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个结果,就可以关闭primeChanclose(primeChan)}()// 遍历primeChan,把结果取出来for {val, ok := <-primeChanif !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 mainimport ("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 := <-intChanif !ok {// 如果读取完毕就跳出循环break}flag := JustPrime(val) // 判断是否为素数if flag {// 如果是素数就往primeChan中添加primeChan <- val}}// 当读取intChan信道完毕,说明没有数据需要判断了exitChan <- true}// 判断num是否为素数
func JustPrime(num int) bool {flag := truefor i := 2; i < num; i++ {if num%i == 0 { // 说明该 num 不是素数flag = falsebreak}}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需要closeclose(primeChan)}()// 读取PrimeChan到切片中var retData []intfor {val, ok := <-primeChanif !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 mainimport ("fmt""time"
)func main() {// 使用select 可以解决从管道取数据的阻塞问题// 1、定义一个管道 10个数据intintChan := make(chan int, 10)for i := 0; i < 10; i++ {intChan <- i}// 2、定义一个管道 5个数据stringstrChan := 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 mainimport ("fmt""time"
)// 函数
func sayHello() {for i := 0; i < 10; i++ {time.Sleep(time.Second)fmt.Println("hello, world")}
}// 函数
func test() {//这里我们可以使用 defer + recoverdefer func() {//捕获 test 抛出的 panicif err := recover(); err != nil {fmt.Println("test() 发生错误", err)}}()//定义了一个 mapvar myMap map[int]stringmyMap[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 mainimport ("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 mainimport ("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

服务端常用API

****** 监听

****** 连接

通过上面的获取的Listener对象,使用该接口的方法Accept()(c Conn, err error)可以获取一个Conn对象,默认情况下这个是阻塞的,只有当有客户端连接请求连接才会返回一个Conn对象。

一个客户端发送连接服务器端就会生成一个新的Conn对象,这个Conn对象负责管理服务器和请求的客户端的连接。通信时都需要这个对象。

服务端并发时,通过协程处理,将Conn对象传入协程执行的函数。

客户端常用API

客户端比较简单,关键是要获取一个Conn对象,用于和服务端连接通信。

获取Conn对象之后,就可以使用Conn的方法,同服务端的Conn的函数一样。

3、Go语言核心编程(高级篇)相关推荐

  1. 视频教程-C语言核心编程-C/C++

    C语言核心编程 夏曹俊:南京捷帝科技有限公司创始人,南京大学计算机硕士毕业,有15年c++跨平台项目研发的经验,领导开发过大量的c++虚拟仿真,计算机视觉,嵌入式图像处理,云安全审计项目,比赛鹰眼系统 ...

  2. C语言核心编程-夏曹俊-专题视频课程

    C语言核心编程-168人已学习 课程介绍         C语言并不是一个高级语言,它实际上属于高级语言与低级语言之间的中间语言,它直接与内存打交道,丰富的数据类型.运算符,但是C语言绝非是一门简单的 ...

  3. 《C#网络编程高级篇之网页游戏辅助程序设计(扫描版)》

    <C#网络编程高级篇之网页游戏辅助程序设计>通过编写C#网络编程语言中具有代表性的实例,向读者深入细致地讲解了如何利用C#语言进行网页游戏辅助程序设计.本书通过大量的代码引导读者一步步学习 ...

  4. R语言学习笔记——高级篇:第十四章-主成分分析和因子分析

    R语言 R语言学习笔记--高级篇:第十四章-主成分分析和因子分析 文章目录 R语言 前言 一.R中的主成分和因子分析 二.主成分分析 2.1.判断主成分的个数 2.2.提取主成分 2.3.主成分旋转 ...

  5. 视频教程-scratch3.0少儿编程(高级篇)4/10猜拳游戏-其他

    scratch3.0少儿编程(高级篇)4/10猜拳游戏 微信企业号星级会员.10多年软件从业经历,国家级软件项目负责人,主要从事软件研发.软件企业员工技能培训.已经取得计算机技术与软件资格考试(软考) ...

  6. go语言核心编程_Go核心编程 - 语言特性(1)

    之前用过一小段时间Go,但是没有系统的学习过,现在想系统的从基础过一遍,为了节约时间,本次学习参考的是 参考李文塔著的<Go语言核心编程>,非我原创 1. Go基础认识 1.1 Go诞生的 ...

  7. Python学习之旅(核心编程基础篇003运算符)

    Python学习之旅 Python核心编程基础篇2020.12.18 一.算数运算符 二.比较运算符 三.赋值运算符 四.逻辑运算符 五.成员运算符 六.身份运算符 七.三目运算符 八.运算符优先级 ...

  8. GO 语言核心编程-全文版

    第 1 章 1.1Golang的学习方向 Go语言,我们可以简单的写成Golang. Golang开山篇 1.2Golang的应用领域 1.2.1区块链的应用开发 1.2.2后台的服务应用 1.2.3 ...

  9. 程序员C语言快速上手——高级篇(十)

    文章目录 高级篇 内存管理 内存四区 内存分配 动态内存管理 指针高级 二维数组 二级指针 函数指针 函数指针的声明 函数指针的赋值与使用 函数指针的传递 void*指针 欢迎关注我的公众号:编程之路 ...

  10. Java 并发编程之美:并发编程高级篇之一-chat

    借用 Java 并发编程实践中的话:编写正确的程序并不容易,而编写正常的并发程序就更难了.相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作 ...

最新文章

  1. 图1 ----节选CEC2015年中结果展示
  2. python是不是特别垃圾-Python 这语言真是混乱和原始
  3. python cnn模型_ZfNet解卷积:可视化CNN模型( PythonCode可视化Cifar10)
  4. 2017年关于数据科学的六大预言
  5. 腾讯地图api修改信息窗口样式_DOTA2 地图编辑器指南(二):总览
  6. 中国豪华的政府大楼VS破学校
  7. 数据科学还是计算机科学_数据科学101
  8. CentOS各个版本镜像下载地址
  9. mysql当时读_Mysql事务以及四中隔离级别实例2以及InnoDB如何解决当时读的幻读问题...
  10. 倍频程分析函数matlab,瞬时声压时域数据怎么用matlab进行1/3倍频程声压级分析
  11. mysql client 升级_解决consider upgrading MySQL client问题
  12. SQLite读写同步之WAL机制
  13. 腾创网络-webrtc视频会议软件
  14. 电脑 蓝屏 问题签名: 问题事件名称: BlueScreen OS 版本: 6.1.7600.2.0.0.256.1 区域设置 ID: 2052...
  15. Centos7 安装mongodb 4.x
  16. 小米10谷歌连携失败_第一批用户反馈小米手表问题多,产品总监发长文解答
  17. 机械振动系统的matlab仿真分析-郭
  18. (HttpClient技术)(58同城系列)58同城登录
  19. 权值线段树+动态开点(学习小结)
  20. “超限”之下,OLED迎来最好的反击

热门文章

  1. WKWebview的内存问题
  2. javascript---继承
  3. 谈java之GUI与安卓
  4. 小用lambda表达式,查询数组里大于80的个数
  5. python unpack 到数列_842. 将数组拆分成斐波那契数列(Python)
  6. idea 中文字体 自动变_提高工作效率,我推荐讯飞语记,瞬间语音秒变文字
  7. idea 修改前后端代码自动运行
  8. DPDK - TX-Offload Checksum
  9. 包分类算法最坏情况下性能比较
  10. 网站服务器无返回数据,服务器无返回数据处理