Loading

01 Go基础

1 go module 设置

https://goproxy.cn/
(1) go module启用
命令行输入: go env -w GO111MODULE=auto

初始化Go moudle,在目录下运行下面命令: go mod init XXX //xxx代表文件夹名

(2) go module代理设置
go env -w GOPROXY=https://goproxy.cn,direct

2 变量

(1) Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用,同一作用域内不支持重复声明,并且Go语言的变量声明后必须使用(
局变量除外)

(2) 变量(Variable)的功能是存储数据,不同的变量保存的数据类型可能会不一样,常见变量的数据类型有: 整型、浮点型、布尔型等。

(3) 变量初始化的标准格式
var 变量名 类型 = 表达式

(4) 有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化
var 变量名 = 表达式

(5) 在函数内部,可以使用更简略的 := 方式声明并初始化变量
变量名 := 表达式

(6) 相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值,常量的声明和变量声明非常类似,只是把var换成了const,常量
在定义的时候必须赋值。例如: const pi = 3.1415

(7) 小结
0) 初始化后的变量在内存中会绑定地址。
1) 函数外的每个语句都必须以关键字开始(var、const、func等)
2) := 不能使用在函数外。
3) _多用于占位,表示忽略值。

3 数据类型

(1) var声明的byte、int、float数据类型的变量零值初始化是0;var声明的string数据类型的变量零值初始化是空字符串;var声明的bool数据类型
的变量零值初始化是false;var声明的数组数据类型的变量零值会进行初始化,例如 var array1 [1]int 的结果是[0]。

(2) 声明的数据类型变量只有初始化后才能使用

(3) 切片
1) var声明的切片数据类型的变量零值 == nil,未初始化。
var a []int
2) 切片数据类型变量的初始化: 在声明切片时初始化,从数组中切片,使用make函数方式(推荐)。
a := make([]T, size, cap)
3) 切片数据类型变量最好使用make函数进行初始化(定义底层数组的长度和容量),避免使用append进行扩容,防止底层引用不同的数组。
4) 数组和切片的区别
切片是对数组的封装;数组数据类型由 [元素数量]<元素类型> 组成,包含长度且长度固定;切片数据类型由 []<元素类型> 组成,包含长度和容量,
可以通过append函数对容量进行扩容;长度代表可以看到和操作的元素数量,容量表示可以看到和操作的元素数量最大是多少。
5) 每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行"扩容",
此时该切片指向的底层数组就会更换。"扩容"操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
6) 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。
7) 示例
示例一:
a := [3]int{1,2}
fmt.Println(a,len(a))

b := make([]int,2,5)
fmt.Println(b,len(b),cap(b))

输出结果:
[1 2 0] 3
[0 0] 2 5

示例二:
a := make([]string,5,10)
fmt.Println(a)
for i := 0; i < 10; i++ {
a=append(a,fmt.Sprintf("%v", i))
}
fmt.Println(a)
fmt.Println(len(a),cap(a))

输出结果:
[ ]
[ 0 1 2 3 4 5 6 7 8 9]
15 20

4 map

(1) var声明的切片数据类型的变量零值 == nil,未初始化
var a map[KeyType]ValueType

(2) map是key:value的格式,key在存储时是无序的

(3) 使用make的方式初始化map
a := make(map[KeyType]ValueType, [cap])
注: 其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

5 函数

(1) 格式
func 函数名(参数)(返回值){
函数体
}
补充: 函数的参数和返回值都是可选的,可以实现一个既不需要参数也没有返回值的函数
func 函数名(){
函数体
}

(2) 注意
1) 函数的参数、返回值都在函数体内。
2) 函数的参数、返回值都支持简写。
3) 函数的返回值可以不写返回值变量名只写返回值变量类型,参数必须都要写。
4) 函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回
示例:
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
5) 函数的返回值变量名和参数变量名之间不能冲突。
6) 函数传入的参数可以不使用,其余的都要对应。

(3) shell
1) 在Shell函数中定义的变量默认也是全局变量,它和在函数外部定义变量拥有一样的效果,要想变量的作用域仅限于
函数内部,可以在定义时加上local命令,此时该变量就成了局部变量。
2) shell函数return返回值必须是整数,return不写时函数默认执行正确是0,错误是非0。

(4) 代码执行顺序
1) 包代码文件全局声明(函数、变量)之间调用可以不考虑顺序。
2) 程序入口代码依次从上往下执行,执行命令必须写在被执行命令的后面。
3) 示例
package main

import "fmt"

func test1() {
fmt.Println("test1")
sum := aa + bb
fmt.Println(sum)
test2()
}

var aa = 2
var bb = 2

func test2() {
fmt.Println("test2")
}

func main() {
test1()
}

输出结果:
test1
4
test2

(5) 高阶函数
高阶函数分为函数作为参数和函数作为返回值两部分。
1) 函数作为参数
示例:
func add(x, y int) int {
return x + y
}
func calc(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
ret2 := calc(10, 20, add)
fmt.Println(ret2) //30
}
注: 一个函数作为另一个函数的参数时关注传入函数的参数和返回值。
一个函数作为另一个函数的返回值时关注返回函数的参数和返回值。

(6) 匿名函数
1) 匿名函数就是没有函数名的函数,匿名函数的定义格式如下
func(参数)(返回值){
函数体
}
2) 匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数。
3) 匿名函数多用于实现回调函数和闭包。
4) 示例
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数

//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}

(7) 函数的闭包
定义一个匿名函数且该匿名函数引用了外层变量。
调用定义的函数需要执行两次并传参。
示例1:
func a(name string) func() {
return func() {
fmt.Println("hello", name)
}
}

r := a("世界") // r此时就是一个闭包
r() // 相当于执行了a函数内部的匿名函数,输出结果为: hello 世界

示例2:
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}

func main() {
jpgFunc := makeSuffixFunc(".jpg") // jpgFunc就是一个闭包
txtFunc := makeSuffixFunc(".txt") // txtFunc就是一个闭包
fmt.Println(jpgFunc("test")) //test.jpg
fmt.Println(txtFunc("test")) //test.txt
}

(8) defer语句
Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就
是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释
放问题。比如:资源清理、文件关闭、解锁及记录时间等。
示例:
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}

输出结果:
start
end
3
2
1

(9) panic/recover
Go语言中目前是没有异常机制,但是使用 panic/recover 模式来处理错误。panic可以在任何地方引发,但 recover 只有在 defer 调用的函数
中有效。
注意:
1) recover() 必须搭配 defer 使用。
2) defer 一定要在可能引发 panic 的语句之前定义。
示例:
package main

import "fmt"

func funcA() {
fmt.Println("func A")
}

func funcB() {
defer func() {
err := recover()
//如果程序出出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
}

func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}

6 变量的作用域

(1) 变量作用域: 全局变量、局部变量

(2) 全局变量
1) 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。在函数中可以访问到全局变量。

(3) 局部变量-局部变量分为两种
1) 局部变量是函数内部定义的变量,函数内定义的变量无法在该函数外使用。
2) 语句块定义的变量,通常我们会在if条件判断、for循环、switch语句上使用这种定义变量的方式。
例如: 语句块变量只在自己的语句块中生效(例如 for i := 0; i < 5; i++ {} 中的i),外层无法访问。

(4) 函数中如果局部变量和全局变量重名,优先访问局部变量。

7 指针和结构体

7.1 指针
参考视频: https://www.bilibili.com/video/BV1TS4y1T7q2?spm_id_from=333.337.search-card.all.click
1 指针是什么
(1) 要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
(2) 任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。
(3) Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和*(根据地址取值)。
(4) 每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。Go语言中的值
类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。

(5) 取变量指针的语法如下
ptr := &v // v的类型为T
v: 代表被取地址的变量,类型为T
ptr: 用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。

(6) b := &a的图示
在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
func main() {
a := 10
b := &a // 取变量a的地址,将指针保存到b中
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
fmt.Printf("type of c:%T\n", c) // type of c:int
fmt.Printf("value of c:%v\n", c) // value of c:10
}

b := &a图示: ptr

(7) 示例

图示1:

image-20220311094048083 图示2: image-20220311134209809

2 指针的意义

图示: image-20220311104026778

3 指针的应用场景
需要变量值同步修改。

场景1: image-20220311104921014 场景2: image-20220311124624131

4 new和make
(1) 说明
在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分
配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。Go语言中new和make是内建的两个函数,
主要用来分配内存。

(2) new
1) 用法
new是一个内置的函数,它的函数签名如下:
func new(Type) *Type
说明:
Type表示类型,new函数只接受一个参数,这个参数是一个类型。
*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

2) 声明一个T的指针类型的变量,零值 == nil,未初始化
// T的指针类型的变量的初始化
var b *int // 声明一个int的指针类型的变量b == nil
b = new(int) // 初始化变量b => int的指针
fmt.Printf("%T %v", b, *b) // *int 0 => int的指针对应的值为int类型的零值
// 说明: var b *int只是声明了一个指针变量b但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。

var c *int = new(int)
fmt.Println(*c) // 0

d := new(int)
fmt.Println(*d) // 0

3) new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}

(3) make
1) 说明
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类
型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下。
func make(t Type, size ...IntegerType) Type

2) 示例
func main() {
var b map[string]int
b = make(map[string]int, 10)
b["沙河娜扎"] = 100
fmt.Println(b)
}
7.2 结构体

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

1 类型别名和自定义类型
(1) 自定义类型
在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型。
自定义类型是定义了一个全新的类型,我们可以基于内置的基本类型定义,也可以通过struct定义。
例如: 将MyInt定义为int类型,通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
type MyInt int

(2) 类型别名
类型别名规定TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师
又会给他起英文名,但这些名字都指的是他本人。
示例:
type TypeAlias = Type

我们之前见过的rune和byte就是类型别名,他们的定义如下:
type byte = uint8
type rune = int32

(3) 自定义类型和类型别名的区别
package main

import "fmt"

// NewInt 类型定义
type NewInt int

// MyInt 类型别名
type MyInt = int

func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) // type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) // type of b:int
}

结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
2 结构体
(1) 说明
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满
足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。也就是我们可以通过struct来
定义自己的类型了。Go语言中通过struct来实现面向对象。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型。

(2) 结构体的定义
使用type和struct关键字来定义结构体,具体代码格式如下:
type 类型名 struct {
字段名 字段类型
字段名 字段类型

}

类型名: 标识自定义结构体的名称,在同一个包内不能重复。
字段名: 表示结构体字段名。结构体中的字段名必须唯一。
字段类型: 表示结构体字段的具体类型。

结构体字段的可见性:
1) 结构体类型名大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
2) 结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

(3) 结构体的定义示例
我们定义一个Person(人)结构体,代码如下:
1) 每个字段单独一行
type person struct {
name string
city string
age int8
}
2) 同样类型的字段写在一行
type person1 struct {
name, city string
age int8
}

说明:
这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够
很方便的在程序中表示和存储人信息了。

(4) 结构体实例化的定义
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。结构体本身也是一种类型,我们可以像声明内置类型一样使
用var关键字声明结构体类型。

var 结构体实例 结构体类型

(5) 结构体实例化与初始化的示例
1) 基本实例化与初始化
type person struct {
name string
city string
age int8
}

func main() {
var p1 person
p1.name = "沙河娜扎"
p1.city = "北京"
p1.age = 18
fmt.Printf("%T\n", p1) //main.person
fmt.Printf("p1=%v\n", p1) //p1={沙河娜扎 北京 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"沙河娜扎", city:"北京", age:18}
}
说明: 我们通过.来访问结构体的字段(成员变量),例如p1.name和p1.age等。

2) 匿名结构体实例化与初始化
在定义一些临时数据结构等场景下还可以使用匿名结构体。
func main() {
var user struct {
Name string
Age int
}
user.Name = "小王子"
user.Age = 18
fmt.Println(user) // {小王子 18}
fmt.Printf("%#v\n", user) // struct { Name string; Age int }{Name:"小王子", Age:18}
}

3) 只实例化没有初始化的结构体,其成员变量都是对应其类型的零值
type person struct {
name string
city string
age int8
}

func main() {
var p4 person
fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}

4) 使用键值对对结构体进行实例化与初始化(常用)
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
type person struct {
name string
city string
age int8
}

func main() {
p5 := person{
name: "小王子",
city: "北京",
age: 18,
}
fmt.Println(p5) //{小王子 北京 18}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小王子", city:"北京", age:18}
}

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
type person struct {
name string
city string
age int8
}

func main() {
p5 := person{
name: "小王子",
}
fmt.Println(p5) //{小王子 0}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小王子", city:"", age:0}
}

5) 使用值的列表实例化与初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值。
type person struct {
name string
city string
age int8
}

func main() {
p8 := person{
"沙河娜扎",
"北京",
28,
}
fmt.Println(p8) // {沙河娜扎 北京 28}
fmt.Printf("p8=%#v\n", p8) //p8=main.person{name:"沙河娜扎", city:"北京", age:28}
}

使用这种格式初始化时,需要注意: 1.必须初始化结构体的所有字段。2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。4.该方式不能和键值
初始化方式混用。

(6) 指针类型结构体实例化与初始化的示例
1) 通过使用new关键字对结构体进行实例化,得到的是结构体的指针
type person struct {
name string
city string
age int8
}

func main() {
// 实例化
var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%v\n", p2) //p2=&{ 0}
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}
// 初始化
(*p2).name = "小王子"
(*p2).city = "上海"
// p2.age = 28其实在底层是(*p2).age = 28,这是Go语言帮我们实现的语法糖。
p2.age = 28
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小王子", city:"上海", age:28}
}

说明:
从打印的结果中我们可以看出p2是一个结构体的指针。
在Go语言中支持对结构体的指针直接使用.来访问结构体的成员。

2) 使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作
type person struct {
name string
city string
age int8
}

func main() {
// 实例化
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%v\n", p3) //p3=&{ 0}
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
// 初始化
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"七米", city:"成都", age:30}
}

3) 对结构体的指针进行键值对实例化与初始化(常用)
使用键值对对结构体的指针进行初始化时,键对应结构体的字段,值对应该字段的初始值。
type person struct {
name string
city string
age int8
}

func main() {
p6 := &person{
name: "小王子",
city: "北京",
age: 18,
}
fmt.Printf("%T\n", p6) //*main.person
fmt.Println(p6) //&{小王子 北京 18}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"小王子", city:"北京", age:18}
}

4) 当某些字段没有初始值的时候,该字段可以不写,此时,没有指定初始值的字段的值就是该字段类型的零值
type person struct {
name string
city string
age int8
}

func main() {
// 实例化并初始化
p7 := &person{
city: "北京",
}
fmt.Printf("%T\n", p7) //*main.person
fmt.Println(p7) //&{ 北京 0}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
}

5) 使用值的列表实例化与初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值。
type person struct {
name string
city string
age int8
}

func main() {
p8 := &person{
"沙河娜扎",
"北京",
28,
}
fmt.Println(p8) //&{沙河娜扎 北京 28}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"沙河娜扎", city:"北京", age:28}
}

使用这种格式初始化时,需要注意: 1.必须初始化结构体的所有字段。2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。4.该方式不能和键值
初始化方式混用。
3 结构体内存布局
结构体占用一块连续的内存。
type test struct {
a, b, c, d int8
}

func main() {
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a) //n.a 0xc0000140a8
fmt.Printf("n.b %p\n", &n.b) //n.b 0xc0000140a9
fmt.Printf("n.c %p\n", &n.c) //n.c 0xc0000140aa
fmt.Printf("n.d %p\n", &n.d) //n.d 0xc0000140ab
}
4 空结构体是不占用空间的
import (
"fmt"
"unsafe"
)

func main() {
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
}
7.3 构造函数
1 说明
Go语言的结构体没有构造函数,我们可以自己实现。
2 构造函数示例
下方的代码就实现了一个person的构造函数。因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是
结构体的指针类型。

//定义一个person结构体
type person struct {
name, city string
age int8
}

// 构造函数: 用于对person结构体进行实例化和初始化操作
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}

func main() {
// 调用newPerson构造函数实例化并初始化一个构造体
p1 := newPerson("泡泡", "东海", 20)
p2 := newPerson("小美美", "东海", 18)
fmt.Printf("p2=%T\n", p2) //p2=*main.person
fmt.Printf("p2=%v\n", p2) //p2=&{小美美 东海 18}
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小美美", city:"东海", age:18}

fmt.Printf("p1=%T\n", p1) //p1=*main.person
fmt.Printf("p1=%v\n", p1) //p1=&{泡泡 东海 20}
fmt.Printf("p1=%#v\n", p1) //p1=&main.person{name:"泡泡", city:"东海", age:20}
}
7.4 方法和接收者
1 说明
(1) Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的
this或者self。方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

(2) 方法的定义格式
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}

1) 接收者变量
接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是self、this之类的命名。例如,Person类型的接收者变量应该
命名为p,Connector类型的接收者变量应该命名为c等。
2) 接收者类型
接收者类型和参数类似,可以是指针类型和非指针类型。
3) 方法名、参数列表、返回参数
具体格式与函数定义相同。
2 值类型的接收者和指针类型的接收者
(1) 值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对
副本,无法修改接收者变量本身。

(2) 指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式
就十分接近于其他语言中面向对象中的this或者self。例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。

(3) 代码示例
package main

import "fmt"

// Person 结构体
type Person struct {
name, city string
age int8
}

// NewPerson 构造函数,用于对Person结构体进行实例化和初始化操作
func NewPerson(name, city string, age int8) *Person {
return &Person{
name: name,
city: city,
age: age,
}
}

// Dream Person 做梦的方法
func (p Person) Dream() {
fmt.Printf("p1=%s的梦想是自由\n", p.name)
}

// SetAge 指针类型的接收者,修改年龄
func (p *Person) SetAge(newAge int8) {
fmt.Printf("p2=%T %v %#v\n", p, p, p)
p.age = newAge
}

// SetAge2 值类型的接收者,修改年龄
func (p Person) SetAge2(newAge int8) {
fmt.Printf("p3=%T %v %#v\n", p, p, p)
p.age = newAge
}

func main() {
fmt.Println("Dream Person 做梦的方法")
p1 := NewPerson("阿酷", "东海", 20)
p1.Dream()

fmt.Println("SetAge 指针类型的接收者,修改年龄")
p2 := NewPerson("双面龟", "东海", 20)
fmt.Printf("p2=%T %v %#v\n", p2, p2, p2)
fmt.Printf("p2=%v\n", p2.age)
p2.SetAge(30)
fmt.Printf("p2=%v\n", p2.age)

fmt.Println("SetAge2 值类型的接收者,修改年龄")
p3 := NewPerson("泡泡", "东海", 20)
fmt.Printf("p3=%T %v %#v\n", p3, p3, p3)
fmt.Printf("p3=%v\n", p3.age)
p3.SetAge2(30)
fmt.Printf("p3=%v\n", p3.age)
}

/*
输出结果:
Dream Person 做梦的方法
p1=阿酷的梦想是自由

SetAge 指针类型的接收者,修改年龄
p2=*main.Person &{双面龟 东海 20} &main.Person{name:"双面龟", city:"东海", age:20}
p2=20
p2=*main.Person &{双面龟 东海 20} &main.Person{name:"双面龟", city:"东海", age:20}
p2=30

SetAge2 值类型的接收者,修改年龄
p3=*main.Person &{泡泡 东海 20} &main.Person{name:"泡泡", city:"东海", age:20}
p3=20
p3=main.Person {泡泡 东海 20} main.Person{name:"泡泡", city:"东海", age:20}
p3=20
*/
3 什么时候应该使用指针类型接收者
(1) 需要修改接收者中的值。
(2) 接收者是拷贝代价比较大的大对象。
(3) 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
7.5 任意类型添加方法
1 说明
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。举个例子,我们基于内置的int类型使用type关键字可以定
义新的自定义类型,然后为我们的自定义类型添加方法。非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

2 示例1
//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}

3 示例2
//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m *MyInt) SayHello(setnum MyInt) {
*m = setnum
fmt.Println("Hello, 我是一个int。")
}

func main() {
var m1 MyInt
m1 = 100
fmt.Println(m1) //100
m1.SayHello(500) //Hello, 我是一个int。
fmt.Printf("%#v %T\n", m1, m1) //500 main.MyInt
}
7.6 结构体的匿名字段
1 说明
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
注意: 这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名
字段只能有一个。

2 示例
//Person 结构体Person类型
type Person struct {
string
int
}

func main() {
p1 := Person{
"小王子",
18,
}
fmt.Printf("%#v\n", p1) //main.Person{string:"小王子", int:18}
fmt.Println(p1.string, p1.int) //小王子 18
}
7.7 嵌套结构体
1 说明
一个结构体中可以嵌套包含另一个结构体或结构体指针。

2 示例1
//Address 地址结构体
type Address struct {
Province string
City string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}

func main() {
user1 := User{
Name: "小王子",
Gender: "男",
Address: Address{
Province: "山东",
City: "威海",
},
}
fmt.Printf("user1=%#v\n", user1)
//user1=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
fmt.Println(user1.Name) //小王子
fmt.Println(user1.Address.City) //威海
}

3 示例2(嵌套匿名字段)
上面user结构体中嵌套的Address结构体也可以采用匿名字段的方式,当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段
中查找。
//Address 地址结构体
type Address struct {
Province string
City string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名字段
}

func main() {
var user2 User
user2.Name = "小王子"
user2.Gender = "男"
user2.Address.Province = "山东" // 匿名字段默认使用类型名作为字段名
user2.City = "威海" // 匿名字段可以省略
fmt.Printf("user2=%#v\n", user2)
//user2=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}

4 嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}

//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}

func main() {
var user3 User
user3.Name = "沙河娜扎"
user3.Gender = "男"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}
7.8 结构体的继承
1 说明
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。

2 示例
//Animal 动物
type Animal struct {
name string
}

func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}

//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}

func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
fmt.Println(d1) //&{4 0xc000042240}
}
7.9 结构体与json序列化
1 说明
(1) JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来
保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。
(2) 序列化
将struct对象转换成json的string格式
(3) 反序列化
将一个json字符串转换成struct对象

2 示例1
package main

import (
"encoding/json"
"fmt"
)

//Student 学生
type Student struct {
ID int
Gender string
Name string
}

//Class 班级
type Class struct {
Title string
Students []*Student
}

func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}

图示: image-20220321114936388

3 示例2
package main

import (
"encoding/json"
"fmt"
)

type Student struct {
ID int
Gender string
Name string
}

type Class struct {
Title string
Students []Student
}

func main() {
c1 := &Class{
Title: "001",
Students: make([]Student, 0, 50),
}

for i := 1; i <= 2; i++ {
stu := Student{
ID: i,
Gender: "男",
Name: fmt.Sprintf("stu%02d", i),
}
c1.Students = append(c1.Students, stu)
}
fmt.Println(c1)
//&{001 [{1 男 stu01} {2 男 stu02}]}

//序列化: 结构体 -> json字符串
data, err := json.Marshal(c1)
if err != nil {
fmt.Println("json marshal failed", err)
return
}
fmt.Printf("json:%s\n", data)
//json:{"Title":"001","Students":[{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"}]}

//反序列化: json字符串 -> 结构体
str := `{"Title":"001","Students":[{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"}]}`
c2 := &Class{} //这里必须使用&符,因为unmarshal传参为指针类型
err = json.Unmarshal([]byte(str), c2)
if err != nil {
fmt.Println("json unmarshal failed", err)
return
}
fmt.Printf("struct:%#v\n", c2)
//struct:&main.Class{Title:"001", Students:[]main.Student{main.Student{ID:1, Gender:"男", Name:"stu01"}, main.Student{ID:2, Gender:"男", Name:"stu02"}}}
fmt.Println(c2)
//&{001 [{1 男 stu01} {2 男 stu02}]}
}
7.10 结构体标签tag
1 说明
(1) 说明
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下。
`key1:"value1" key2:"value2"`

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用
空格分隔。

注意事项:
为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过
反射也无法正确取值。例如不要在key和value之间添加空格。

应用:
结构体的json序列化默认使用字段名作为key,结构体字段名小写(代表私有)不能被json包访问,前端需要使用小写的key,这时就需要使用结构体
的tag标签实现json序列化该字段时的key。

2 示例(为Student结构体的每个字段定义json序列化时使用的Tag)
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}

func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "沙河娜扎",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}
7.11 知识拾遗
1 说明
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。

2 普通类型
package main

import "fmt"

func modify(x int) {
x = 100
}

func main() {
a := 10
modify(a)
fmt.Println(a) // 10
}

3 切片类型(引用类型slice或map)
package main

import "fmt"

func modify(x []string) {
x[1] = "88"
}

func main() {
sliceTest := []string{"1", "2"}
sliceTest[0] = "66"
fmt.Println(sliceTest) // [66 2]
modify(sliceTest)
fmt.Println(sliceTest) // [66 88]
}

4 结构体中使用切片不正确的做法
package main

import "fmt"

type person struct {
name string
age int8
dreams []string
}

func (p *person) setDreams(dreams []string) {
p.dreams = dreams
}

func main() {
p1 := person{name: "小王子", age: 18}
data := []string{"吃饭", "睡觉", "打豆豆"}
p1.setDreams(data)
fmt.Println(p1.dreams) // [吃饭 睡觉 打豆豆]

// 你真的想要修改 p1.dreams 吗
data[1] = "不睡觉"
fmt.Println(data) // [吃饭 不睡觉 打豆豆]
fmt.Println(p1.dreams) // [吃饭 不睡觉 打豆豆]
}

5 结构体中使用切片正确的做法
在方法中使用传入的slice的拷贝进行结构体赋值
同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题
package main

import "fmt"

type person struct {
name string
age int8
dreams []string
}

func (p *person) setDreams(dreams []string) {
p.dreams = make([]string, len(dreams))
copy(p.dreams, dreams)
}

func main() {
p1 := person{name: "小王子", age: 18}
data := []string{"吃饭", "睡觉", "打豆豆"}
p1.setDreams(data)
fmt.Println(p1.dreams) // [吃饭 睡觉 打豆豆]

// 你真的想要修改 p1.dreams 吗
data[1] = "不睡觉"
fmt.Println(data) // [吃饭 不睡觉 打豆豆]
fmt.Println(p1.dreams) // [吃饭 睡觉 打豆豆]
}

8 包和接口

8.1 包
1 init 函数
(1) init函数没有参数也没有返回值,我们也不能在代码中主动调用它
(2) init函数在包导入的时候自动执行
(3) init函数多用来做一些初始化的操作
(4) 包内的init函数是在全局声明(变量、函数)之后执行的,init函数在包导入的时候自动执行
(5) init可以应用于任意包中,且可以重复定义多个
(6) main函数只能用于main包中,且只能定义一个,包名为main的包是应用程序的入口包,这种包编译后会得到一个可执行文
件,而编译不包含main包的源代码则不会得到可执行文件。
(7) 一个包的初始化过程是按照代码中引入的顺序来进行的,所有在该包中声明的init函数都将被串行调用并且仅调用执行一次。
每一个包初始化的时候都是先执行依赖的包中声明的init函数再执行当前包中声明的init函数。确保在程序的main函数开始执行
时所有的依赖包都已初始化完成。
(8) 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)。
(9) 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序。

图示1: image-20220320231113289

图示2: image-20220320231734552

2 go语言包
(1) 包可以理解为一个文件夹下存放多个go文件,该文件以"package <包名>"开头
(2) 在一个包中通常将实现不同功能的代码放到不同的文件中,这样逻辑会更清晰
(3) 同一个包中的不同文件可以直接调用
(4) 不允许循环引用包和导入包而不使用
(5) 当导入的包重名时可以给包起别名
(6) 包内标识符首字母大写表示对外可见,不对外的标识符都要首字母小写
注: 标识符
在编程语言中标识符就是程序员定义的具有特殊意义的词,比如变量名、常量名、函数名等等。Go语言中标识符由字母数字和_(下划线)组成
,并且只能以字母和_开头。
(7) 包的导入包括单行导入和多行导入,通常使用多行导入
(8) 导入包却不使用它,只想使用包中的init函数对包资源进行初始化,叫做匿名导入包(import _)
8.2 接口

1 说明

(1) 什么是接口
1) 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
2) 在Go语言中接口(interface)是一种类型,一种抽象的类型。
3) 相较于之前章节中讲到的那些具体类型(字符串、切片、结构体等)更注重“我是谁”,接口类型更注重“我能做什么”的问题。接口类型就像是一种
约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编程方式实现解耦。

(2) 接口类型
接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。相较于使用结构体类型,当我们使用接口类型
说明相比于它是什么更关心它能做什么。

(3) 接口的定义
每个接口类型由任意个方法签名组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2

}

1) 接口类型名
Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型
含义。
2) 方法名
当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
3) 参数列表、返回值列表
参数列表和返回值列表中的参数变量名可以省略。

定义一个包含Write方法的Writer接口示例:
type Writer interface{
Write([]byte) error
}
当你看到一个Writer接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write方法来做一些事情。

(4) 实现接口的条件
1) 接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。
2) 接口示例
我们定义的Singer接口类型,它包含一个Sing方法:
// Singer 接口
type Singer interface {
Sing()
}

我们有一个Bird结构体类型如下:
type Bird struct {}

因为Singer接口只包含一个Sing方法,所以只需要给Bird结构体添加一个Sing方法就可以满足Singer接口的要求:
// Sing Bird类型的Sing方法
func (b Bird) Sing() {
fmt.Println("汪汪汪")
}

这样就称为Bird实现了Singer接口。

2 接口基础

package main

import "fmt"

// 01接口基础
/*
(1) 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
(2) 在Go语言中接口(interface)是一种类型,一种抽象的类型。相较于那些具体类型(字符串、切片、结构体等)更注重“我是谁”,
接口类型更注重“我能做什么”的问题。接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编
程方式实现解耦。
(3) 接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。
(4) 相较于使用结构体类型,当我们使用接口类型说明相比于它是什么更关心它能做什么。
(5) 定义接口格式
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2

}
1) 接口类型名: Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有关闭操作的接口叫Closer等,接口名最好要能突出该接口的类型含义。
2) 方法名: 当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
3) 参数列表、返回值列表: 参数列表和返回值列表中的参数变量名可以省略。
(6) 接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。
(7) 接口类型变量: 一个接口类型的变量能够存储所有实现了该接口的类型变量。
*/

// 我们定义的singer接口类型,它包含一个sing方法
type singer interface {
sing()
}

// 我们有一个bird结构体类型如下
type bird struct {
name string
}

// 因为singer接口只包含一个sing方法,所以只需要给bird结构体添加一个sing方法就可以满足singer接口的要求
// 这样就称为bird实现了singer接口
func (b *bird) sing() {
fmt.Printf("%s啾啾的叫\n", b.name)
}

// 我们有一个dog结构体类型如下
type dog struct {
name string
}

// dog实现了singer接口
func (d *dog) sing() {
fmt.Printf("%s汪汪的叫\n", d.name)
}

func main() {
// dog和bird类型均实现了singer接口,此时一个singer类型的变量就能够接收dog和bird类型的变量。
// 狗叫
var dogObj singer = &dog{name: "中华田园犬"}
dogObj.sing() //输出结果: 中华田园犬汪汪的叫
// 鸟叫
var birdObj singer = &bird{name: "黄鹂鸟"}
birdObj.sing() //输出结果: 黄鹂鸟啾啾的叫

// 值接收者和指针接收者
// 在定义结构体方法时既可以使用值接收者也可以使用指针接收者,那么对于实现接口来说也有值接收者和指针接收者
// (1) 接口方法: 对于值接收者实现的接口,无论使用值类型还是指针类型都没有问题。对于指针接收者实现的接口,只能使用指针类型。
// (2) 结构体方法: 对于值接收者和指针接收者,无论使用值类型还是指针类型都没有问题。
// (3) 值接收者和指针接收者的区别: 值接收者不能修改结构体内的数据(值拷贝开销大),指针接收者能修改结构体内的数据(内存地址引用,开销小)。
}

3 一个类型实现多个接口

package main

import "fmt"

// 02 一个类型实现多个接口

/*
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如猫不仅可以叫,还可以动。我们完全可以分别定义sayer接口和mover接口。
*/

// sayer接口
type sayer interface {
say()
}

// mover接口
type mover interface {
move()
}

// cat 结构体
type cat struct {
name string
}

// cat既可以实现sayer接口,也可以实现mover接口
// cat实现sayer接口
func (c *cat) say() {
fmt.Printf("%s会喵喵的叫\n", c.name)
}

// cat实现mover接口
func (c *cat) move() {
fmt.Printf("%s会动\n", c.name)
}

func main() {
c := &cat{name: "小花猫"}
var s sayer = c
var m mover = c
// 对sayer类型调用say方法
s.say() // 小花猫会喵喵的叫
// 对mover类型调用move方法
m.move() // 小花猫会动
}

4 多种类型实现同一接口

package main

import "fmt"

// 03 多种类型实现同一接口
/*
Go语言中不同的类型还可以实现同一接口。例如在我们的代码世界中不仅狗可以动,汽车也可以动。
*/

// mover03接口
type mover03 interface {
move()
}

// dog03结构体
type dog03 struct {
name string
}

// dog03实现mover03接口
func (d *dog03) move() {
fmt.Printf("%s会动\n", d.name)
}

// car03结构体
type car03 struct {
name string
}

// car03实现mover03接口
func (c *car03) move() {
fmt.Printf("%s速度70迈,心情是自由自在\n", c.name)
}

func main() {
// 这样我们在代码中就可以将狗和汽车当成一个会动的类型来处理,不必关注它们具体是什么,只需要调用它们的move方法就可以了
var dog03obj mover03 = &dog03{name: "大黄狗"}
dog03obj.move() // 大黄狗会动

var car03obj mover03 = &car03{name: "宝马汽车"}
car03obj.move() // 宝马汽车速度70迈,心情是自由自在
}

5 接口组合

package main

import "fmt"

// 04 接口组合

/*
(1) 接口与接口之间可以通过互相嵌套形成新的接口类型,例如Go标准库io源码中就有很多接口之间互相组合的示例。
(2) 对于这种由多个接口类型组合形成的新接口类型,同样只需要实现新接口类型中规定的所有方法就算实现了该接口类型。
*/

// 定义mover04接口
type mover04 interface {
move()
}

// 定义singer04接口
type singer04 interface {
sing()
}

// 定义actions04接口
type actions04 interface {
mover04
singer04
}

// 定义cat04结构体
type cat04 struct {
name string
}

// cat04实现mover04接口
func (c *cat04) move() {
fmt.Printf("%s会动\n", c.name)
}

// cat04实现singer04接口
func (c *cat04) sing() {
fmt.Printf("%s会喵喵的叫\n", c.name)
}

func main() {
var cat04obj actions04 = &cat04{name: "小花猫"}
cat04obj.sing() // 小花猫会喵喵的叫
cat04obj.move() // 小花猫会动
}

6 空接口

package main

import "fmt"

// 05 空接口

/*
(1) 空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
(2) 我们在使用空接口类型时不必使用type关键字声明。
var any interface{} <=> type any interface{}
*/

// 5.1 空接口示例
// any是不包含任何方法的空接口类型
type any interface {
}

// dog05结构体
type dog05 struct {
}

// 5.2 空接口作为函数的参数
// 使用空接口实现可以接收任意类型的函数参数。
func show(a interface{}) {
fmt.Printf("空接口作为函数的参数type:%T value:%v\n", a, a)
}

func main() {
// 5.1 空接口示例
var x any
// 字符串类型
x = "你好"
fmt.Printf("字符串类型type:%T value:%v\n", x, x) // 字符串类型type:string value:你好
// int类型
x = 100
fmt.Printf("int类型type:%T value:%v\n", x, x) // int类型type:int value:100
// 布尔类型
x = true
fmt.Printf("布尔类型type:%T value:%v\n", x, x) // 布尔类型type:bool value:true
// 结构体类型
x = dog05{}
fmt.Printf("结构体类型type:%T value:%v\n", x, x) // 结构体类型type:main.dog05 value:{}

// 5.2 空接口作为函数的参数
// 传入int类型值为100
show(100) // 空接口作为函数的参数type:int value:100

// 5.3 空接口作为map的值
// 使用空接口实现可以保存任意值的字典。
var studentInfo = make(map[string]interface{}, 10)
// value是字符串类型
studentInfo["name"] = "小王子"
// value是int类型
studentInfo["age"] = 18
// value是布尔类型
studentInfo["married"] = false
fmt.Printf("type:%T value:%v\n", studentInfo, studentInfo) // type:map[string]interface {} value:map[age:18 married:false name:小王子]
}

7 接口值

package main

import "fmt"

// 06 接口值

/*
(1) 由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体值之外,还需要记录这个值属于的类型。也就是说
接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型和动态值。
*/

// mover06接口
type mover06 interface {
move()
}

// dog06结构体
type dog06 struct {
name string
}

// dog06实现mover06接口
func (d *dog06) move() {
fmt.Printf("%s狗在跑\n", d.name)
}

func main() {
// 创建一个mover06接口类型的变量m
var m mover06
// 此时,接口变量m是接口类型的零值,也就是它的类型和值部分都是nil
fmt.Println(m == nil) // true
fmt.Printf("type:%T value:%v\n", m, m) // type:<nil> value:<nil>
// 注意: 我们不能对一个空接口值调用任何方法,否则会产生panic
//m.move() // panic: runtime error: invalid memory address or nil pointer dereference

// 将一个*dog06结构体指针赋值给变量m
m = &dog06{name: "旺财"}
// 此时,接口值m的动态类型会被设置为*dog06,动态值为结构体变量的指针
fmt.Printf("type:%T value:%v\n", m, m) // type:*main.dog06 value:&{旺财}
// 调用方法
m.move() // 旺财狗在跑
}

8 类型断言

package main

import "fmt"

// 07 类型断言

/*
接口值可能赋值为任意类型的值,那我们如何从接口值获取其存储的具体数据呢?我们可以借助标准库fmt包的格式化打印获取到接口值的动态类型。
*/

// mover07接口
type mover07 interface {
move()
}

// dog07结构体
type dog07 struct {
name string
}

// dog06实现mover06接口
func (d *dog07) move() {
fmt.Printf("%s狗在跑\n", d.name)
}

// 如果对一个接口值有多个实际类型需要判断,推荐使用switch语句来实现。
// justifyType 对传入的空接口类型变量x进行类型断言
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
case *dog07:
fmt.Printf("x is a *dog07 is %v\n", v)
default:
fmt.Println("unsupported type!")
}
}

func main() {
var dog07obj mover07 = &dog07{name: "旺财"}
// fmt包内部其实是使用反射的机制在程序运行时获取到动态类型的名称
fmt.Printf("%T\n", dog07obj) // *main.dog07

// 从接口值中获取到对应的实际值需要使用类型断言
// 语法: x.(T) x表示接口类型的变量,T表示断言x可能是的类型。
// 该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。
v, ok := dog07obj.(*dog07)
if ok {
fmt.Println("类型断言成功")
fmt.Println(v) // &{旺财}
v.name = "富贵"
fmt.Println(v) // &{富贵}
} else {
fmt.Println("类型断言失败")
}

// switch语句来实现断言
justifyType(dog07obj) // x is a *dog07 is &{富贵}
}

9 error接口

package main

import "fmt"

// 08 error接口

/*
(1) Go语言中的错误处理与其他语言不太一样,它把错误当成一种值来处理,更强调判断错误、处理错误,而不是一股脑的 catch 捕获异常。
*/

// 8.1Error接口
// Go 语言中使用一个名为 error 接口来表示错误类型。
// error 接口只包含一个方法——Error,这个函数需要返回一个描述错误信息的字符串。
//type error interface {
// Error() string
//}

// 当一个函数或方法需要返回错误时,我们通常是把错误作为最后一个返回值。例如下面标准库 os 中打开文件的函数。
//func Open(name string) (*File, error) {
// return OpenFile(name, O_RDONLY, 0)
//}

// 8.2创建错误
// 我们可以根据需求自定义error,最简单的方式是使用errors包提供的New函数创建一个错误。errors.New
//func New(text string) error {
// return &errorString{text}
//}
// 它接收一个字符串参数返回包含该字符串的错误。我们可以在函数返回时快速创建一个错误。
//func queryById(id int64) (*Info, error) {
// if id <= 0 {
// return nil, errors.New("无效的id")
// }
//
// // ...
//}
// 或者用来定义一个错误变量,例如标准库io.EOF错误定义如下。
//var EOF = errors.New("EOF")

// 8.4错误结构体类型
// 我们还可以自己定义结构体类型,实现error接口。

// OpError 自定义结构体类型
type OpError struct {
Op string
}

// Error OpError 类型实现error接口
func (e *OpError) Error() string {
return fmt.Sprintf("无权执行%s操作", e.Op)
}

func main() {
// 8.1Error接口
// 由于 error 是一个接口类型,默认零值为nil。所以我们通常将调用函数返回的错误与nil进行比较,以此来判断函数是否返回错误。例如你会经常看到类似下面的错误判断代码。
// 注意: 当我们使用fmt包打印错误时会自动调用 error 类型的 Error 方法,也就是会打印出错误的描述信息。
//file, err := os.Open("./xx.go")
//if err != nil {
// fmt.Println("打开文件失败,err:", err)
// return
//}

// 8.3fmt.Errorf
// 当我们需要传入格式化的错误描述信息时,使用fmt.Errorf是个更好的选择。
//fmt.Errorf("查询数据库失败,err:%v", err)
// 但是上面的方式会丢失原有的错误类型,只拿到错误描述的文本信息。
// 为了不丢失函数调用的错误链,使用fmt.Errorf时搭配使用特殊的格式化动词%w,可以实现基于已有的错误再包装得到一个新的错误。
//fmt.Errorf("查询数据库失败,err:%w", err)
// 对于这种二次包装的错误,errors包中提供了以下三个方法。
//func Unwrap(err error) error // 获得err包含下一层错误
//func Is(err, target error) bool // 判断err是否包含target
//func As(err error, target interface{}) bool // 判断err是否为target类型

// 8.4错误结构体类型
var errObj error = &OpError{"删除"}
fmt.Printf("type:%T value:%s\n", errObj, errObj) // type:*main.OpError value:无权执行删除操作
fmt.Println(errObj) // 无权执行删除操作
fmt.Println(errObj.Error()) // 无权执行删除操作
}

9 channel

package main

import (
"fmt"
"sync"
"time"
)

// 03 channel基础
/*
(1) 单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
(2) 虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,很多并发模型中必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
(3) Go语言采用的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
(4) 如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。
(5) Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
(6) 通道至少同时有一个发送者和接收者(分别对应两个goroutine),否则通道会出现阻塞,报通道死锁问题。
(7) 发送方关闭通道后,接收方从通道中取不到值时,多返回值分别为通道对应类型的零值和状态false,表示通道已经关闭。
*/

// 3.5 无缓冲的通道
// 循环从通道ch中接收所有值,直到通道被关闭后退出
func receive(ch chan int) {
for {
v, ok := <-ch
if !ok {
fmt.Println("通道已关闭")
break
}
fmt.Printf("v:%v ok:%v\n", v, ok)
}
}

// 3.7 多返回值模式
// 循环从通道ch中接收所有值,直到通道被关闭后退出
// 发送方关闭通道后,接收方从通道中取不到值时,多返回值分别为通道对应类型的零值和状态false,表示通道已经关闭。
func f2(ch chan int) {
for {
v, ok := <-ch
if !ok {
fmt.Println("通道已关闭")
break
}
fmt.Printf("v:%v ok:%v\n", v, ok)
}
}

// 3.8 for range接收值
// 使用for range循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后自动退出循环
func f3(ch chan int) {
for v := range ch {
fmt.Printf("v:%v ok:true\n", v)
}
fmt.Println("通道已关闭")
}

// 3.9 单向通道
/*说明:
(1)作为producer函数的提供者,我们在返回通道的时候可能只希望调用方拿到返回的通道后只能对其进行接收操作。
正常情况下consumer函数中只会对通道进行接收操作,但是这不代表不可以在consumer函数中对通道进行发送操作。
Go语言中提供了单向通道来处理这种需要限制通道只能进行某种操作的情况。
(2) 语法
<-chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收
箭头<-和关键字chan的相对位置表明了当前通道允许的操作,这种限制将在编译阶段进行检测。另外对一个只接收通道
执行close也是不允许的,因为默认通道的关闭操作应该由发送方来完成。
*/

// producer 返回一个通道,并持续将符合条件的数据发送至返回的通道中,数据发送完成后会将返回的通道关闭
// producer 返回一个只接收通道,从代码层面限制了该函数返回的通道只能进行接收操作,保证了数据安全。
func producer() <-chan int {
ch := make(chan int, 2)
// 创建一个新的goroutine执行发送数据的任务
go func() {
for i := 0; i < 10; i++ {
if i%2 == 1 {
fmt.Printf("i:%v ", i)
ch <- i
}
}
// 任务完成后关闭通道
close(ch)
}()
return ch
}

// consumer 从通道中接收数据进行计算
func consumer(ch <-chan int) int {
sum := 0
for v := range ch {
sum += v
//sum = sum + v
}
return sum
}

// 3.11 select多路复用
// 在终端打印出10以内的奇数
func selectUse() {
ch := make(chan int, 1)
for i := 1; i <= 10; i++ {
select {
case x := <-ch:
fmt.Printf("x:%v ", x)
case ch <- i:
}
}
}

/*代码首先是创建了一个缓冲区大小为1的通道 ch,进入 for 循环后:
(1) 第一次循环时 i = 1,select 语句中包含两个 case 分支,此时由于通道中没有值可以接收,所以x := <-ch 这个 case 分支不满足,
而ch <- i这个分支可以执行,会把1发送到通道中,结束本次 for 循环。
(2) 第二次 for 循环时,i = 2,由于通道缓冲区已满,所以ch <- i这个分支不满足,而x := <-ch这个分支可以执行,从通道接收值1并赋
值给变量 x ,所以会在终端打印出 1。
(3) 后续的 for 循环以此类推会依次打印出3、5、7、9。
*/

// 3.12 通道误用示例
// 通道误用导致的bug示例一
func channelBug1() {
wg := sync.WaitGroup{}

ch := make(chan int, 10)
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)

wg.Add(3)
for j := 0; j < 3; j++ {
go func() {
for {
task := <-ch
// 这里假设对接收的数据执行某些操作
fmt.Println(task)
}
wg.Done()
}()
}
wg.Wait()
}

/*说明:
将上述代码编译执行后,匿名函数所在的 goroutine 并不会按照预期在通道被关闭后退出。
因为task := <- ch的接收操作在通道被关闭后会一直接收到零值,而不会退出。此处的接
收操作应该使用task, ok := <- ch,通过判断布尔值ok为假时退出;或者使用 select
来处理通道。
*/

// 通道误用导致的bug示例一修复
//func channelBug1Repair() {
// wg := sync.WaitGroup{}
//
// ch := make(chan int, 10)
// for i := 0; i < 10; i++ {
// ch <- i
// }
// close(ch)
//
// wg.Add(3)
// for j := 0; j < 3; j++ {
// go func() {
// for {
// task, ok := <-ch
// if ok {
// // 这里假设对接收的数据执行某些操作
// fmt.Println(task)
// } else {
// break
// }
// }
// wg.Done()
// }()
// }
// wg.Wait()
//}

func channelBug1Repair() {
wg := sync.WaitGroup{}

ch := make(chan int, 10)
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)

wg.Add(3)
for j := 0; j < 3; j++ {
go func() {
for task := range ch {
// 这里假设对接收的数据执行某些操作
fmt.Println(task)
}
wg.Done()
}()
}
wg.Wait()
}

// 通道误用导致的bug示例二
func channelBug2() {
ch := make(chan string)
go func() {
// 这里假设执行一些耗时的操作
time.Sleep(3 * time.Second)
ch <- "job result"
}()

select {
case result := <-ch:
fmt.Println(result)
case <-time.After(time.Second): // 较小的超时时间
return
}
}

/*说明:
上述代码片段可能导致 goroutine 泄露(goroutine 并未按预期退出并销毁)。
由于 select 命中了超时逻辑,导致通道没有消费者(无接收操作),而其定义的
通道为无缓冲通道,因此 goroutine 中的ch <- "job result"操作会一直
阻塞,最终导致 goroutine 泄露。
*/

func main() {
// 3.1 channel类型
// channel是 Go 语言中一种特有的类型,声明通道类型变量的格式为: var 变量名称 chan 元素类型
// chan: 是关键字 元素类型: 是指通道中传递元素的类型
// 声明一个传递整型的通道
//var ch1 chan int
// 声明一个传递布尔型的通道
//var ch2 chan bool
// 声明一个传递int切片的通道
//var ch3 chan []int

// 3.2 channel零值
// 未初始化的通道类型变量其默认零值是nil
//var ch chan int
//fmt.Println(ch == nil) // true

// 3.3 初始化channel
// 声明的通道类型变量需要使用内置的make函数初始化之后才能使用,具体格式为: make(chan 元素类型, [缓冲大小])
// channel的缓冲大小是可选的
//ch4 := make(chan int)
// 声明一个缓冲区大小为1的通道
//ch5 := make(chan bool, 1)

// 3.4 channel操作
// 通道共有发送(send)、接收(receive)和关闭(close)三种操作。而发送和接收操作都使用 <- 符号。
// 定义一个通道
//ch := make(chan int)

// 将一个值发送到通道中
//ch <- 10 // 把10发送到ch中

// 从一个通道中接收值
//x := <-ch // 从ch中接收值并赋值给变量x
//<-ch // 从ch中接收值,忽略结果

// 调用内置的close函数来关闭通道
//close(ch)
/*关闭通道注意事项:
(1) 一个通道值是可以被垃圾回收掉的。通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。
它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
(2) 关闭后的通道有以下特点
1) 对一个关闭的通道再发送值就会导致 panic。
2) 对一个关闭的通道进行接收会一直获取值直到通道为空。
3) 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4) 关闭一个已经关闭的通道会导致 panic。
*/

// 3.5 无缓冲的通道
// 无缓冲的通道又称为阻塞的通道

//ch := make(chan int)
//ch <- 10
//fmt.Println("发送成功")
// 上面这段代码能够通过编译,但是执行的时候会出现以下错误
/*错误分析:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
D:/Code/GoCode/01GoBase/05并发/03channel基础.go:85 +0x31
1) deadlock表示我们程序中的 goroutine 都被挂起导致程序死锁了,为什么会出现deadlock错误呢。
2) 因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,
否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收
操作阻塞。就像田径比赛中的4x100接力赛,想要完成交棒必须有一个能够接棒的运动员,否则只能等待。简单来说就是无缓冲的通
道必须有至少一个接收方才能发送成功。
3) 上面的代码会阻塞在ch <- 10这一行代码形成死锁,可行的解决方法是创建一个 goroutine 去接收值。
*/

//ch := make(chan int)
//// 创建一个 goroutine 从通道接收值
//go receive(ch)
//ch <- 10
//ch <- 20
//ch <- 30
//close(ch)
//fmt.Println("发送成功")
/*输出结果:
v:10 ok:true
v:20 ok:true
v:30 ok:true
通道已关闭
发送成功
*/
/*说明:
(1) 首先无缓冲通道ch上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,
这时数字10才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方所
在的 goroutine 将阻塞,直到 main goroutine 中向该通道发送数字10。
(2) 使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道。
*/

// 3.6 有缓冲的通道
// 还有另外一种解决无缓冲的通道阻塞的方法,那就是使用有缓冲区的通道。我们可以在使用 make 函数初始化通道时,可以为其指定通道的容量。

// 创建一个容量为1的有缓冲区通道
//ch := make(chan int, 1)
//ch <- 10
//ret := <-ch
//fmt.Println("发送成功")
//fmt.Println(ret)
/*输出结果:
发送成功
10
*/
/*说明:
(1) 只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。
当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。就像你小
区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
(2) 我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。
*/

// 3.7 多返回值模式
// (1) 当向通道中发送完数据时,我们可以通过close函数来关闭通道。当一个通道被关闭后,
// 再往该通道发送值会引发panic,从该通道取值的操作会先取完通道中的值。通道内的值被接
// 收完后再对通道执行接收操作得到的值会一直都是对应元素类型的零值。那我们如何判断一个
// 通道是否被关闭了呢。
// (2) 对一个通道执行接收操作时支持使用多返回值模式。

//ch := make(chan int, 1)
//ch <- 10
//// 关闭ch通道,否则会导致死锁,因为定义的ch通道缓存数为1,通道存值也为1,下面通道取值时取了两次
//close(ch)
//// ch通道关闭后从ch通道取值,ch通道为空
//value, ok := <-ch
//// ch通道关闭后从ch通道取不到值会得到对应类型的零值
//value1, ok1 := <-ch
///*说明:
//(1) value: 从通道中取出的值,如果通道被关闭则返回对应类型的零值。
//(2) ok: 通道ch关闭时返回 false,否则返回 true。
//*/
//fmt.Println(value, ok, value1, ok1) // 10 true 0 false

// 下面代码片段中的f2函数会循环从通道ch中接收所有值,直到通道被关闭后退出
//ch := make(chan int, 2)
//ch <- 1
//ch <- 2
//close(ch)
//f2(ch)
//fmt.Println("发送成功")
/*输出结果:
v:1 ok:true
v:2 ok:true
通道已关闭
发送成功
*/

// 3.8 for range接收值
// 通常我们会选择使用for range循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后自动退出循环。
// 上面那个for循环示例我们使用for range改写后会很简洁。
// 注意: 目前Go语言中并没有提供一个不对通道进行读取操作就能判断通道是否被关闭的方法。不能简单的通过len(ch)操作来判断通道是否被关闭。

//ch := make(chan int, 2)
//ch <- 1
//ch <- 2
//close(ch)
//f3(ch)
//fmt.Println("发送成功")
/*输出结果:
v:1 ok:true
v:2 ok:true
通道已关闭
发送成功
*/

// 3.9 单向通道
/*说明:
在某些场景下我们可能会将通道作为参数在多个任务函数间进行传递,通常我们会选择在不同的任务函数中对通道的使用进行限制,比如限制通道在某个函数
中只能执行发送或只能执行接收操作。想象一下,我们现在有Producer和Consumer两个函数,其中Producer函数会返回一个通道,并且会持续将符合条
件的数据发送至该通道,并在发送完成后将该通道关闭。而Consumer函数的任务是从通道中接收值进行计算,这两个函数之间通过Processer函数返回的
通道进行通信。
*/

//// 启动一个goroutine协程在后台往通道中存符合条件的值,存完后关闭通道
//ch := producer()
//// 同时main主协程从通道中循环取值,直到通道中的值被取完为止,计算所有取值的和
//res := consumer(ch)
//fmt.Printf("sum:%v", res) // i:1 i:3 i:5 i:7 i:9 sum:25

// 在函数传参及任何赋值操作中全向通道(正常通道)可以转换为单向通道,但是无法反向转换
//ch3 := make(chan int, 1)
//ch3 <- 10
//close(ch3)
//// 函数传参时将ch3转为单项通道
//res3 := consumer(ch3)
//fmt.Println(res3) // 10
//
//ch4 := make(chan int, 1)
//ch4 <- 20
//// 声明一个只接收通道ch5
//var ch5 <-chan int
//// 变量赋值时将ch4转为单向通道
//ch5 = ch4
//res5 := <-ch5
//fmt.Println(res5) // 20

// 3.10 总结
// 下面的表格中总结了对不同状态下的通道执行相应操作的结果
// ./03channel基础.png
// 注意:对已经关闭的通道再执行 close 也会引发 panic。

// 3.11 select多路复用
/*说明:
(1) 某些场景下我们可能需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以被接收那么当前 goroutine 将会发生阻塞。
(2) Go 语言内置了select关键字,使用它可以同时响应多个通道的操作。
(3) select 的使用方式类似于之前学到的 switch 语句,它也有一系列 case 分支和一个默认的分支。每个 case 分支会对应一个通道
的通信(接收或发送)过程。select 会一直等待,直到其中的某个 case 的通信操作完成时,就会执行该 case 分支对应的语句。具体格式如下:
select {
case <-ch1:
//...
case data := <-ch2:
//...
case ch3 <- 10:
//...
default:
//默认操作
}
(4) select 语句具有以下特点:
1) 可处理一个或多个 channel 的发送/接收操作。
2) 如果多个 case 同时满足,select 会随机选择一个执行。
3) 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。
*/

// 在终端打印出10以内的奇数
//selectUse() // x:1 x:3 x:5 x:7 x:9

// 3.12 通道误用示例
// 通道误用导致的bug示例一
//channelBug1()

// 通道误用导致的bug示例一修复
//channelBug1Repair()

// 通道误用导致的bug示例二
channelBug2()
}

10 并发编程

10.1 基本概念
1 并发编程在当前软件领域是一个非常重要的概念,随着CPU等硬件的发展,我们无一例外的想让我们的程序运行的快一点、再快一点。Go语言在语言层面
天生支持并发,充分利用现代CPU的多核优势,这也是Go语言能够大范围流行的一个很重要的原因。

2 串行、并发与并行
串行:我们都是先读小学,小学毕业后再读初中,读完初中再读高中。
并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。
并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。

3 进程、线程和协程
进程(process):程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
线程(thread):操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位。
协程(coroutine):非操作系统提供而是由用户自行创建和控制的用户态‘线程’,比线程更轻量级。

4 并发模型
业界将如何实现并发编程总结归纳为各式各样的并发模型,常见的并发模型有以下几种:
线程&锁模型
Actor模型
CSP模型
Fork&Join模型
Go语言中的并发程序主要是通过基于CSP(communicating sequential processes)的goroutine和channel来实现,当然也支持使用传统的多
线程共享内存的并发方式。
10.2 goroutine

1 说明

(1) Goroutine 是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始
其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度,goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将m
个goroutine合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。
(2) Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自
动创建。
(3) 在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能——goroutine,当你需要让某个任务并发执行的时候,你只需要把
这个任务包装成一个函数,开启一个 goroutine 去执行这个函数就可以了,就是这么简单粗暴。

2 go关键字

(1) Go语言中使用goroutine非常简单,只需要在函数或方法调用前加上go关键字就可以创建一个goroutine,从而让该函数或方法在新创建的
goroutine中执行
go f() // 创建一个新的 goroutine 运行函数f

(2) 匿名函数也支持使用go关键字创建 goroutine 去执行
go func(){
// ...
}()

(3) 注意
一个 goroutine 必定对应一个函数/方法,可以创建多个 goroutine 去执行相同的函数/方法。

3 启动单个goroutine

启动 goroutine 的方式非常简单,只需要在调用函数(普通函数和匿名函数)前加上一个go关键字。

(1) 在 main 函数中执行普通函数调用的示例
package main

import (
"fmt"
)

func hello() {
fmt.Println("hello")
}

func main() {
hello()
fmt.Println("你好")
}

将上面的代码编译后执行,得到的结果如下:
hello
你好

代码中hello函数和其后面的打印语句是串行的。

图示: goroutine01

(2) 我们在调用 hello 函数前面加上关键字go,也就是启动一个 goroutine 去执行 hello 这个函数
package main

import (
"fmt"
)

func hello() {
fmt.Println("hello")
}

func main() {
go hello() //启动另外一个goroutine去执行hello函数
fmt.Println("你好")
}
将上述代码重新编译后执行,得到输出结果如下:
你好

这一次的执行结果只在终端打印了”你好”,并没有打印 hello。这是为什么呢?
其实在 Go 程序启动时,Go 程序就会为 main 函数创建一个默认的 goroutine。在上面的代码中我们在 main 函数中使用 go 关键字创建了另外
一个 goroutine 去执行 hello 函数,而此时 main goroutine还在继续往下执行,我们的程序中此时存在两个并发执行的goroutine。当main
函数结束时整个程序也就结束了,同时 main goroutine 也结束了,所有由 main goroutine 创建的 goroutine 也会一同退出。也就是说我们
的 main 函数退出太快,另外一个 goroutine 中的函数还未执行完程序就退出了,导致未打印出“hello”。
main goroutine 就像是《权利的游戏》中的夜王,其他的 goroutine 都是夜王转化出的异鬼,夜王一死它转化的那些异鬼也就全部GG了。
所以我们要想办法让 main 函数“等一等”在另一个 goroutine 中运行的 hello 函数。其中最简单粗暴的方式就是在 main 函数中“time.Sleep”
一秒钟(这里的1秒钟只是我们为了保证新的 goroutine 能够被正常创建和执行而设置的一个值)。

代码示例:
package main

import (
"fmt"
"time"
)

func hello() {
fmt.Println("hello")
}

func main() {
go hello()
fmt.Println("你好")
time.Sleep(time.Second)
}
将我们的程序重新编译后再次执行,程序会在终端输出如下结果,并且会短暂停顿一会儿:
你好
hello

为什么会先打印你好呢?
这是因为在程序中创建 goroutine 执行函数需要一定的开销,而与此同时 main 函数所在的 goroutine 是继续执行的。

图示: goroutine02

(3) sync.WaitGroup方式
在上面的程序中使用time.Sleep让 main goroutine 等待 hello goroutine执行结束是不优雅的,当然也是不准确的。Go 语言中通过sync包为
我们提供了一些常用的并发原语,比如 sync 包中的WaitGroup。当你并不关心并发操作的结果或者有其它方式收集并发操作的结果时,WaitGroup是
实现等待一组并发操作完成的好方法。

在 main goroutine 中使用sync.WaitGroup来等待 hello goroutine 完成后再退出示例:
package main

import (
"fmt"
"sync"
)

// 声明全局等待组变量
var wg sync.WaitGroup

func hello() {
fmt.Println("hello")
wg.Done() // 告知当前goroutine完成
}

func main() {
wg.Add(1) // 登记1个goroutine
go hello()
fmt.Println("你好")
wg.Wait() // 阻塞等待登记的goroutine完成
}
将上述代码重新编译后执行,得到输出结果如下:
你好
hello

将代码编译后再执行,得到的输出结果和之前使用"time.Sleep"一致,但是这一次程序不再会有多余的停顿,hello goroutine 执行完毕后程序直
接退出。

4 启动多个goroutine

(1) 说明
在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine。让我们再来看一个新的代码示例。这里同样使用了sync.WaitGroup来实现
goroutine的同步。

(2) 代码示例
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup

func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("hello", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}

多次执行上面的代码会发现每次终端上打印数字的顺序都不一致。这是因为10个 goroutine 是并发执行的,而 goroutine 的调度是随机的。

5 动态栈

操作系统的线程一般都有固定的栈内存(通常为2MB),而 Go 语言中的 goroutine 非常轻量级,一个 goroutine 的初始栈空间很小(一般为2KB)
,所以在 Go 语言中一次创建数万个 goroutine 也是可能的。并且 goroutine 的栈不是固定的,可以根据需要动态地增大或缩小,Go的runtime
会自动为 goroutine 分配合适的栈空间。

6 goroutine调度

(1) 操作系统的线程会被操作系统内核调度时会挂起当前执行的线程并将它的寄存器内容保存到内存中,选出下一次要执行的线程并从内存中恢复该线
程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。从一个线程切换到另一个线程需要完整的上下文切换。因为可能需要多次内存访问,索引
这个切换上下文的操作开销较大,会增加运行的cpu周期。

(2) 区别于操作系统内核调度操作系统线程,goroutine 的调度是Go语言运行时(runtime)层面的实现,是完全由 Go 语言本身实现的一套调度系
统——go scheduler。它的作用是按照一定的规则将所有的 goroutine 调度到操作系统线程上执行。

(3) 在经历数个版本的迭代之后,目前 Go 语言的调度器采用的是 GPM 调度模型。

(4) 单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine 则是由Go运行时(runtime)自己的调度器调度
的,完全是在用户态下完成的,不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池,不直接调用系统
的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上
,再加上本身 goroutine 的超轻量级,以上种种特性保证了 goroutine 调度方面的性能。

(5) goroutine调度器原理
1) G:表示 goroutine,每执行一次go f()就创建一个 G,包含要执行的函数和上下文信息。
全局队列(Global Queue):存放等待运行的 G。

2) P:表示 goroutine 执行所需的资源,最多有 GOMAXPROCS 个。
P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G 时,G 优先加入到 P 的本地队列,如果本地队列
满了会批量移动部分 G 到全局队列。

3) M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M运
行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
Goroutine 调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。

图示: gpm

7 GOMAXPROCS

(1) 说明
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的
机器上,GOMAXPROCS 默认为 8。Go语言中可以通过runtime.GOMAXPROCS函数设置当前程序并发时占用的 CPU逻辑核心数。(Go1.5版本之前,默认
使用的是单核心执行。Go1.5 版本之后,默认使用全部的CPU 逻辑核心数。)

(2) 示例
package main

import (
"fmt"
"runtime"
"sync"
)

var wg sync.WaitGroup

func main() {
runtime.GOMAXPROCS(8)
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}

8 并发安全和锁

(1) 说明
有时候我们的代码中可能会存在多个 goroutine 同时操作一个资源(临界区)的情况,这种情况下就会发生竞态问题(数据竞态)。这就好比现实生
活中十字路口被各个方向的汽车竞争,还有火车上的卫生间被车厢里的人竞争。

数据竞争的示例:
package main

import (
"fmt"
"sync"
)

var (
x int64

wg sync.WaitGroup // 等待组
)

// add 对全局变量x执行5000次加1操作
func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}

func main() {
wg.Add(2)

go add()
go add()

wg.Wait()
fmt.Println(x)
}

我们将上面的代码编译后执行,不出意外每次执行都会输出诸如9537、5865、6527等不同的结果。这是为什么呢?
在上面的示例代码片中,我们开启了两个 goroutine 分别执行 add 函数,这两个 goroutine 在访问和修改全局的x变量时就会存在数据竞争,某个
goroutine 中对全局变量x的修改可能会覆盖掉另一个 goroutine 中的操作,所以导致最后的结果与预期不符。

(2) 互斥锁
1) 说明
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个 goroutine 可以访问共享资源。Go 语言中使用sync包中提供的Mutex类型来实现互斥锁。sync.Mutex提供了两个方法供我们使用。
方法名 功能
func (m *Mutex) Lock() 获取互斥锁
func (m *Mutex) Unlock() 释放互斥锁
2) 使用互斥锁限制每次只有一个 goroutine 才能修改全局变量x
package main

import (
"fmt"
"sync"
)

// sync.Mutex

var (
x int64

wg sync.WaitGroup // 等待组

m sync.Mutex // 互斥锁
)

// add 对全局变量x执行5000次加1操作
func add() {
for i := 0; i < 5000; i++ {
m.Lock() // 修改x前加锁
x = x + 1
m.Unlock() // 改完解锁
}
wg.Done()
}

func main() {
wg.Add(2)

go add()
go add()

wg.Wait()
fmt.Println(x)
}

将上面的代码编译后多次执行,每一次都会得到预期中的结果——10000。

3) 使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等待锁;当互斥锁释放后,等待的 goroutine 才
可以获取锁进入临界区,多个 goroutine 同时等待一个锁时,唤醒的策略是随机的。
(3) 读写互斥锁
1) 说明
互斥锁是完全互斥的,但是实际上有很多场景是读多写少的,当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的,这种场景下使
用读写锁是更好的一种选择。读写锁在 Go 语言中使用sync包中的RWMutex类型。
sync.RWMutex提供了以下5个方法。
读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;而
当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。
方法名 功能
func (rw *RWMutex) Lock() 获取写锁
func (rw *RWMutex) Unlock() 释放写锁
func (rw *RWMutex) RLock() 获取读锁
func (rw *RWMutex) RUnlock() 释放读锁
func (rw *RWMutex) RLocker() Locker 返回一个实现Locker接口的读写锁
2) 使用代码构造一个读多写少的场景,然后分别使用互斥锁和读写锁查看它们的性能差异
我们假设每一次读操作都会耗时1ms,而每一次写操作会耗时10ms,我们分别测试使用互斥锁和读写互斥锁执行10次并发写和1000次并发读的耗时数据。
package main

import (
"fmt"
"sync"
"time"
)

var (
x int64
wg sync.WaitGroup
mutex sync.Mutex
rwMutex sync.RWMutex
)

// writeWithLock 使用互斥锁的写操作
func writeWithLock() {
mutex.Lock() // 加互斥锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设写操作耗时10毫秒
mutex.Unlock() // 解互斥锁
wg.Done()
}

// readWithLock 使用互斥锁的读操作
func readWithLock() {
mutex.Lock() // 加互斥锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
mutex.Unlock() // 释放互斥锁
wg.Done()
}

// writeWithRWLock 使用读写互斥锁的写操作
func writeWithRWLock() {
rwMutex.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设写操作耗时10毫秒
rwMutex.Unlock() // 释放写锁
wg.Done()
}

// readWithRWLock 使用读写互斥锁的读操作
func readWithRWLock() {
rwMutex.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwMutex.RUnlock() // 释放读锁
wg.Done()
}

func do(wf, rf func(), wc, rc int) {
start := time.Now()
// wc个并发写操作
for i := 0; i < wc; i++ {
wg.Add(1)
go wf()
}

// rc个并发读操作
for i := 0; i < rc; i++ {
wg.Add(1)
go rf()
}

wg.Wait()
cost := time.Since(start)
fmt.Printf("x:%v cost:%v\n", x, cost)

}
func main() {
// 使用互斥锁,10并发写,1000并发读
do(writeWithLock, readWithLock, 10, 1000) //x:10 cost:15.711616s
// 使用读写互斥锁,10并发写,1000并发读
do(writeWithRWLock, readWithRWLock, 10, 1000) //x:20 cost:169.8234ms
}

从最终的执行结果可以看出,使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能。不过需要注意的是如果一个程序中的读操作和写操作数量
级差别不大,那么读写互斥锁的优势就发挥不出来。

9 sync.WaitGroup

(1) 说明
在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。sync.WaitGroup有以下几个方法。
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了 N 个并发任务时,就将计数器值增加N。每个任务完成时通
过调用 Done 方法将计数器减1。通过调用 Wait 来等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成。
方法名 功能
func (wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0
(2) 代码示例
var wg sync.WaitGroup

func hello() {
defer wg.Done()
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(1)
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
wg.Wait()
}

注意: sync.WaitGroup是一个结构体,进行参数传递的时候要传递指针。

10 sync.Once

(1) 说明
在某些场景下我们需要确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案——sync.Once,sync.Once只有一个Do方法,其签名如下:
func (o *Once) Do(f func())

注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。

(2) 加载配置文件示例
延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的
启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。

var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}

// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons())
return icons[name]
}

(3) 并发安全的单例模式
借助sync.Once实现的并发安全的单例模式
package main

import (
"fmt"
"sync"
)

type singleton struct {
name string
}

var instance *singleton
var once sync.Once

func GetInstance(name string) *singleton {
once.Do(func() {
instance = &singleton{name: name}
})
return instance
}
func main() {
fmt.Println(GetInstance("lc")) //lc
fmt.Println(GetInstance("刘畅")) //lc
}

(4) sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始
化操作的时候是并发安全的并且初始化操作也不会被执行多次。

11 sync.Map

(1) 说明
Go 语言中内置的 map 不是并发安全的,代码示例如下,将下面的代码编译后执行,会报出fatal error: concurrent map writes错误。我们不
能在多个 goroutine 中并发对内置的 map 进行读写操作,否则会存在数据竞争问题。

(2) 为 map 加锁来保证并发的安全性
package main

import (
"fmt"
"strconv"
"sync"
)

var m = make(map[string]int)
var mutex sync.Mutex

func get(key string) int {
return m[key]
}

func set(key string, value int) {
m[key] = value
}

func main() {
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
mutex.Lock()
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
mutex.Unlock()
wg.Done()
}(i)
}
wg.Wait()
}

(3) 使用sync.Map来保证map并发的安全性
Go语言的sync包中提供了一个开箱即用的并发安全版 map——sync.Map。开箱即用表示其不用像内置的 map 一样使用 make 函数初始化就能直接使用
。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
方法名 功能
func (m *Map) Store(key, value interface{}) 存储key-value数据
func (m *Map) Load(key interface{}) (value interface{}, ok bool) 查询key对应的value
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) 查询或存储key对应的value
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) 查询并删除key
func (m *Map) Delete(key interface{}) 删除key
func (m *Map) Range(f func(key, value interface{}) bool) 对map中的每个key-value依次调用f
package main

import (
"fmt"
"strconv"
"sync"
)

// 并发安全的map
var m = sync.Map{}

func main() {
wg := sync.WaitGroup{}
// 对m执行20个并发的读写操作
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n) // 存储key-value
value, _ := m.Load(key) // 根据key取值
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}

11 网络编程

11.1 互联网分层模型
互联网的逻辑实现被分为好几层。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。用户接触到的只是最上面的那一层,根本不会感觉到
下面的几层。要理解互联网就需要自下而上理解每一层的实现的功能。如下图所示,互联网按照不同的模型划分会有不同的分层,但是不论按照什么模型去
划分,越往上的层越靠近用户,越往下的层越靠近硬件。在软件开发中我们使用最多的是下图中将互联网划分为五个分层的模型。

图示: image-20220321224448655

11.2 数据传输
如下图所示,发送方的HTTP数据经过互联网的传输过程中会依次添加各层协议的标头信息,接收方收到数据包之后再依次根据协议解包得到数据。

图示: image-20220321225223308

11.3 socket编程
Socket是BSD UNIX的进程通信机制,通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄。Socket可以理解为TCP/IP网络的API,它
定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。电脑上运行的应用程序通常通过”套接字”向网络发出请求或者应答网络请求。

1 socket图解

Socket是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket后面
,对用户来说只需要调用Socket规定的相关函数,让Socket去组织符合指定的协议数据然后进行通信。

图示: image-20220321230426223

2 Go语言实现TCP/UDP通信

socket这一块在实际的编码中很少用到,由框架替代(比如gin),这里就省略了。

13 总结

13.1 slice、map使用make进行初始化
(1) slice
a := make([]T, size, cap)
(2) map
a := make(map[KeyType]ValueType, [cap])
(3) 总结
slice: size代表长度,表示可以看到和操作的元素数量。
slice、map: cap代表容量, 表示可以看到和操作的元素数量最大是多少,会自动扩容,看不到初始值,可以不管。

切片图示: image-20220313164112324

13.2 函数参数变量名和返回值变量名
参数变量名:   必须有
返回值变量名: 可加可不加
13.3 变量
(1) 函数内部声明的变量必须使用,函数外部声明的全局变量可以不使用。

(2) 变量声明之标准式
格式: var <变量名> <变量类型> = <变量值>
范围: 全局、函数内

(3) 变量声明之推导式
格式: var <变量名> = <变量值>
范围: 全局、函数内

(4) 变量声明之简明式
格式: <变量名> := <变量值>
范围: 函数内

(5) const关键字
1) const表示声明常量的关键字
2) 可以使用标准式或推导式声明常量
3) 声明的常量可以不使用
13.4 指针
(1) 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

(2) 变量、指针地址、指针变量、取地址、取值的相互关系和特性如下
1) 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
2) 指针变量的值是指针地址。
3) 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

(3) 指针地址和指针取值
1) 示例一
package main

import "fmt"

func main() {
// 指针测试
a := "中国"
b := &a
fmt.Printf("b=%v\n", b) //b=0xc000042230
fmt.Printf("&a=%v\n", &a) //&a=0xc000042230
fmt.Printf("*b=%v\n", *b) //*b=中国
fmt.Printf("&*b=%v\n", &*b) //&*b=0xc000042230
fmt.Printf("&b=%v\n", &b) //&b=0xc000006028
}

2) 示例二
package main

import "fmt"

// 值类型
func modify(a string) {
fmt.Printf("a=%T %v %#v\n", a, a, a)
a = "北京"
}

// 指针类型
func modify2(b *string) {
fmt.Printf("b=%T %v %#v\n", b, b, b)
*b = "北京"
}

func main() {
fmt.Println("值类型......")
a := "上海"
fmt.Printf("a=%v\n", a)
modify(a)
fmt.Printf("a=%v\n", a)

fmt.Println("指针类型......")
b := "上海"
fmt.Printf("b=%v\n", b)
modify2(&b)
fmt.Printf("b=%v\n", b)
}

/*
输出结果:
值类型......
a=上海
a=string 上海 "上海"
a=上海
指针类型......
b=上海
b=*string 0xc000042280 (*string)(0xc000042280)
b=北京
*/
13.5 new与make的区别
(0) new与make对引用类型(slice、map、channel、指针)的变量声明分配内存空间(初始化);对于值类型(int、float、bool、string、
array、struct)的变量声明不需要分配内存空间,因为它们在声明的时候已经默认分配好了内存空间;引用类型变量的零值是nil,值类型变量
的零值是类型零值。
(1) 二者都是用来做内存分配的。
(2) make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身。
(3) 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

(4) 其它
1) 指针 = 内存地址
2) 指针地址、指针类型、指针取值
3) 声明一个int的指针类型的变量a,其值为int的指针,该int指针对应的值为该类型的零值
var a = new(int)
fmt.Printf("%T %v", a, *a) // *int 0
4) 变量: 变量名 变量类型 变量值 指针地址
13.6 数据类型
指针类型: 默认值nil、引用类型

图示: image-20220314203410951

13.7 数组、切片、map
(1) 数组
1) 定义
var 数组变量名 [元素数量]T
比如:var a [5]int,数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。[5]int和[10]int是不同的类型。
数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会panic。
2) 数组是同一种数据类型元素的集合,是值类型。
3) 有序可重复,通过下标取值和修改值,不能动态扩容(改、查)。
数组支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
[n]*T表示指针数组,*[n]T表示数组指针。
相同类型的赋值拷贝引用不同的内存空间。
4) 初始化
方法一:
初始化数组时可以使用初始化列表来设置数组元素的值。
func main() {
var testArray [3]int //数组会初始化为int类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2 0]
fmt.Println(cityArray) //[北京 上海 深圳]
}
方法二:
按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,例如:
func main() {
var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2]
fmt.Printf("type of numArray:%T\n", numArray) //type of numArray:[2]int
fmt.Println(cityArray) //[北京 上海 深圳]
fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
}
方法三:
我们还可以使用指定索引值的方式来初始化数组,例如:
func main() {
a := [...]int{1: 1, 3: 5}
fmt.Println(a) // [0 1 0 5]
fmt.Printf("type of a:%T\n", a) //type of a:[4]int
}

5) 数组的遍历
遍历数组a有以下两种方法:
func main() {
var a = [...]string{"北京", "上海", "深圳"}
// 方法1:for循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}

// 方法2:for range遍历
for index, value := range a {
fmt.Println(index, value)
}
}

6) 多维数组
Go语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。
示例:
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
}
注意: 多维数组只有第一层可以使用...来让编译器推导数组长度

(2) 切片
1) 定义
var name []T
name表示变量名,T表示切片中的元素类型。
它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作
一块数据集合。
2) 切片(Slice)是一个拥有相同类型元素的可变长度的序列。
3) 有序可重复,通过下标取值和修改值,可以动态扩容(增、删、改、查)。
切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。切片唯一合法的比较操作是和nil比较。一个nil值的切片并
没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:
var s1 []int //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。
相同类型的赋值拷贝引用相同的内存空间。
4) 初始化
方法一:
func main() {
// 声明切片类型
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(a) //[]
fmt.Println(b) //[]
fmt.Println(c) //[false true]
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
// fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil比较
}
方法二:
make([]T, size, cap)
T: 切片的元素类型
size: 切片中元素的数量
cap: 切片的容量

func main() {
a := make([]int, 2, 10)
fmt.Println(a) //[0 0]
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //10
}
上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。

5) 使用copy()函数复制切片
首先我们来看一个问题(切片的赋值拷贝):
func main() {
a := []int{1, 2, 3, 4, 5}
b := a
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 3 4 5]
b[0] = 1000
fmt.Println(a) //[1000 2 3 4 5]
fmt.Println(b) //[1000 2 3 4 5]
}
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:
copy(destSlice, srcSlice []T)
srcSlice: 数据来源切片
destSlice: 目标切片

示例:
func main() {
// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
}

6) 从切片中删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}

总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

7) append()方法为切片添加元素
Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
func main(){
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
}

8) 切片遍历
切片的遍历方式和数组是一致的,支持索引遍历和for range遍历。

示例:
func main() {
s := []int{1, 3, 5}

for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}

for index, value := range s {
fmt.Println(index, value)
}
}

(3) map(key:value)
1) 定义
map[KeyType]ValueType
KeyType: 表示键的类型。
ValueType: 表示键对应的值的类型。
2) map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
3) key无序不重复,通过key取value值和修改value值,可以动态扩容(增、删、改、查)。
map唯一合法的比较操作是和nil比较。
相同类型的赋值拷贝引用相同的内存空间。
4) 初始化
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
方法一:
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
方法二:
map也支持在声明的时候填充元素
func main() {
userInfo := map[string]string{
"username": "沙河小王子",
"password": "123456",
}
fmt.Println(userInfo) //
}

5) 使用delete()函数删除键值对
使用delete()内建函数从map中删除一组键值对,delete()函数的格式如下:
delete(map, key)
map:表示要删除键值对的map
key:表示要删除的键值对的键

示例:
func main(){
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
delete(scoreMap, "小明")//将小明:100从map中删除
for k,v := range scoreMap{
fmt.Println(k, v)
}
}

6) 判断某个键是否存在
Go语言中有个判断map中键是否存在的特殊写法,格式如下:
value, ok := map[key]

示例:
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}

7) map的遍历
Go语言中使用for range遍历map。

示例:
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
注意:遍历map时的元素顺序与添加键值对的顺序无关。

(4) 综合
1) 元素为map类型的切片
func main() {
// 元素为map类型的切片
var a = []map[string]int{
{"1": 1, "2": 2, "3": 3},
{"a": 1, "b": 2, "d": 4},
}
fmt.Println(a) //[map[1:1 2:2 3:3] map[a:1 b:2 d:4]]

var mapSlice = make([]map[string]string, 3)
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "小王子"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "沙河"
fmt.Println(mapSlice) //[map[address:沙河 name:小王子 password:123456] map[] map[]]
}

2) 值为切片类型的map
func main() {
// 值为切片类型的map
var a = map[string][]string{
"中国": {"北京", "上海"},
}
fmt.Println(a) // map[中国:[北京 上海]]

var sliceMap = make(map[string][]string, 3)
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap) //map[中国:[北京 上海]]
}
13.8 结构体、构造函数、方法
1 结构体
结构体需要实例化和初始化才能够使用,一般使用"键值对"方式对结构体进行实例化和初始化。
2 构造函数
用于对结构体进行实例化和初始化操作,类似于python编程中class中的init。
因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以构造函数建议返回的是结构体的指针类型。
3 方法
对实例化和初始化结构体对象的绑定,类似于python编程中的self(当前对象的引用,需要使用指针类型的接收者,否则无法修改对象的属性)。

(1) 结构体构造函数是值类型
方法接收者类型可以是值类型(方法内不可修改结构体实例化对象),也可以是指针类型(方法内可修改结构体实例化对象)。
(2) 结构体构造函数是指针类型
方法接收者类型可以是值类型(方法内不可修改结构体实例化对象),也可以是指针类型(方法内可修改结构体实例化对象)。
4 接口
(1) go语言中的接口是一种类型,是一种抽象的类型。
(2) 接口不管你是什么类型,它只管你要实现什么方法。
(3) 定义一个接口,只要实现了接口中定义的方法的类型都可以称为这个接口的类型。

空接口的应用:
(1) 空接口类型作为函数的参数
(2) 空接口类型作为map的value类型
(3) 定义一个空接口变量 var x interface{}
(4) 任意类型都实现了空接口,空接口变量可以存储任意值。

(1) 结构体构造函数是值类型
方法接收者类型可以是值类型(方法内不可修改结构体实例化对象)。
(2) 结构体构造函数是指针类型
方法接收者类型可以是值类型(方法内不可修改结构体实例化对象),也可以是指针类型(方法内可修改结构体实例化对象)。
5 指针类型和值类型
(1) 值类型(资源开销大)
此情况下函数传参是值拷贝,传入变量和接收变量在内存地址上是没有任何联系的。
(2) 指针类型(资源开销小)
此情况下函数传参不是值拷贝,传入变量和接收变量在内存地址上是有联系的,接收变量的值是传入变量的内存地址。
(3) 当构造函数返回的是结构体的指针类型的变量p(值是结构体的指针)时,(*p).<字段名> <=> p.<字段名>,go语言做了语法糖底层会自动转换。
(4) 结构体每一次实例化、初始化都会在内存单独开辟一块独立的内存。
13.9 流程控制
1 if else(分支结构)
(1) Go语言中if条件判断的格式如下:
if 表达式1 {
分支1
} else if 表达式2 {
分支2
} else{
分支3
}

当表达式1的结果为true时,执行分支1,否则判断表达式2,如果满足则执行分支2,都不满足时,则执行分支3。if判断中的else if和else都是可选
的,可以根据实际需要进行选择。从上往下匹配,满足条件立刻返回结果。

(2) 一般写法示例
func main() {
score := 65
if score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
}

(3) 特殊写法示例
在 if 表达式之前添加一个执行语句,再根据变量值进行判断,限制变量的作用域在if块内。
func main() {
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
}

2 for(循环结构)
Go 语言中的所有循环类型均可以使用for关键字来完成。for循环可以通过break、goto、return、panic语句强制退出循环。
(1) for循环的基本格式如下
for 初始语句;条件表达式;结束语句{
循环体语句
}

(2) 条件表达式返回true时循环体不停地进行循环,直到条件表达式返回false时自动退出循环
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}

(3) for循环的初始语句可以被忽略,但是初始语句后的分号必须要写
func main() {
i := 0
for ; i < 10; i++ {
fmt.Println(i)
}
}

(4) for循环的初始语句和结束语句都可以省略
这种写法类似于其他编程语言中的while,在while后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。
func main() {
i := 0
for i < 10 {
fmt.Println(i)
i++
}
}

(5) 无限循环
for {
循环体语句
}

3 for range(键值循环)
(1) 说明
Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel)。 通过for range遍历的返回值有以下规律:
数组、切片、字符串返回索引和值。
map返回键和值。
通道(channel)只返回通道内的值。

(2) 示例
func main() {
var a = [1]int{1}
for k, v := range a {
fmt.Println(k, v)
}
}

4 switch case
(1) 说明
使用switch语句可方便地对大量的值进行条件判断。Go语言规定每个switch只能有一个default分支。
从上往下匹配,满足条件立刻返回结果。

(2) 示例
func main() {
finger := 3
switch finger {
case 1:
fmt.Println("大拇指")
case 2:
fmt.Println("食指")
case 3:
fmt.Println("中指")
case 4:
fmt.Println("无名指")
case 5:
fmt.Println("小拇指")
default:
fmt.Println("无效的输入!")
}
}

(3) 一个分支可以有多个值,多个case值中间使用英文逗号分隔
func main() {
switch n := 7; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
}

(4) 分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量
func main() {
age := 30
switch {
case age < 25:
fmt.Println("好好学习吧")
case age > 25 && age < 35:
fmt.Println("好好工作吧")
case age > 60:
fmt.Println("好好享受吧")
default:
fmt.Println("活着真好")
}
}

5 break(跳出循环)
(1) 说明
break语句可以结束for、switch和select的代码块。break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在
对应的for、switch和 select的代码块上。

(2) 示例
func main() {
for i := 0; i < 5; i++ {
if i == 2 {
break
}
fmt.Println(i) // 0 1
}
}

6 continue(继续下次循环)
(1) 说明
continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for循环内使用。在continue语句后添加标签时,表示开始标签对应的循环。

(2) 示例
func main() {
for i := 0; i < 5; i++ {
if i == 2 {
continue
}
fmt.Println(i) // 0 1 3 4
}
}
13.10 并发编程
1 goroutine
(1) 并发和并行
并发: 一颗cpu在同一时刻只能处理一个任务,如果有多个任务需要处理,那么cpu就需要在一段时间内分别在不同的任务
之间来回切换执行(cpu时间分片),那么有的任务执行的快,有的任务执行的慢。
并行: 如果有多颗cpu,多颗cpu同时执行多个任务,任务之间同时执行。

(2) 同步阻塞
主goroutine和子goroutine是并发执行任务的,可能主goroutine任务执行完成后子goroutine的任务还没有执行
完成,此时主goroutine的退出导致子goroutine被杀掉,解决的办法是同步阻塞等待(var wg sync.WaitGroup),
在主goroutine添加计数牌(wg.Add(<num>))记录开启子goroutine的数量,每当一个子goroutine任务结束后计数
牌减1(wg.Done()),当计数牌为零时说明所有的子goroutine任务都完成了,此时主goroutine解除阻塞(wg.Wait())。

(3) 锁
互斥锁(用在读写相差不大的场景下): 同一时刻只有一个goroutine可以获取锁。
读写互斥锁(用在读多写少的场景下): 永读锁,写互斥锁。


posted @ 2022-05-15 00:53  云起时。  阅读(210)  评论(0编辑  收藏  举报