• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

3、Go语言核心编程高级篇

武飞扬头像
Golang-Study
帮助1

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 type
	field2 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 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{} // {}和上面一样可以给字段赋值

学新通

*** 四种方式说明

  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 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))
}
学新通

学新通

  1. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
    学新通
  2. 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转。
    学新通
  3. struct 的每个字段上,可以写上一个 tag, 该 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 基本介绍

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

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)
}
学新通

学新通

说明

  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 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 方法和函数的区别

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

4、工厂模式

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

需求引入

如果在另外一个包中定义了一个结构体,如果结构体的名称的首字母是大写,那么在其他包中可以顺利的创建对象;如果首字母是小写,那么就无法在其他包中创建对象,为了解决这个问题,使用工厂模式的方式。
学新通
学新通
此外,如果结构体中的字段的名称的首字母是小写的,其他包肯定也无法访问,为了解决这个问题,可以通过方法来解决。代码修改如下:

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、面向对象三大特性之封装

*** 基本介绍
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 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()
}
学新通

学新通

*** 继承深入探讨

  1. 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。
    当然上面的说法仅限于两个结构体在同一个包中,如果嵌套的匿名结构体在另外一个包中,那么就必须是大写字母开头才可以访问。
    学新通学新通
  2. 匿名结构体字段访问可以简化
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()
}
学新通

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

学新通

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

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

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"	// 正确

}
学新通
  1. 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字。这种模式就不是继承关系,只能当做一个成员,按照普通的成员使用即可。

学新通

  1. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值。
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、接口(面向接口编程)

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

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 接口概念

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 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)

}
学新通

学新通

  1. 两种特殊的情况
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()
}

学新通

学新通
代码小结:

  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 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、文件的基本介绍

*** 文件概念
文件,对我们并不陌生,文件是数据源(保存数据的地方)的一种,比如大家经常使用的 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 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 一次将整个文件读入到内存中),这种方式适用于文件不大的情况。相关方法和函数(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()
}

学新通

学新通

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

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 的序列化

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

*** 序列化函数
在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()

}
学新通

学新通

*** 注意事项
对于结构体的序列化,如果我们希望序列化后的 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 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)
	}
}

学新通

学新通
小结:

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

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)
	}
}

学新通

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

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)
	}
}

学新通

学新通
总结:

  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 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)
}
学新通

*** 基本注意事项

  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 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

服务端常用API

****** 监听
学新通学新通
学新通

****** 连接

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

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

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

客户端常用API

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

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

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgekjke
系列文章
更多 icon
同类精品
更多 icon
继续加载