1 并发编程1
1.1 汇总
01.常用信息1
a.进程和线程
A.进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
B.线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。
b.并发和并行
A. 多线程程序在一个核的cpu上运行,就是并发。
B. 多线程程序在多个核的cpu上运行,就是并行。
并发不是并行:并发主要由切换时间片来实现"同时"运行,并行则是直接利用多核实现多线程的运行,go可以设置使用核数,以发挥多核计算机的能力。
c.协程和线程
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。
d.goroutine
goroutine只是由官方实现的超级"线程池"。
每个实力4~5KB的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因。
goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
02.常用信息2
a.说明
Golang 的调度器使用了 GMP 模型(Goroutine、Machine、Processor)来管理并发任务。
这个模型解决了传统多线程调度器的性能问题,并优化了协程的调度方式
b.Golang 调度器的演变
a.单进程时代
特点: 计算机只能一个任务一个任务地处理,阻塞会导致 CPU 时间浪费。
b.多进程/线程时代
并发能力: 当一个进程阻塞时,系统可以切换到其他进程继续执行。
问题: 进程的创建、切换、销毁开销大,导致 CPU 调度开销高。
c.协程的引入
概念: 协程是一种轻量级的线程,它在用户态进行调度,不会像线程那样频繁地进行上下文切换,减少了内存占用和调度开销。
关系:
N:1: 多个协程绑定到一个线程。优点是切换快速,缺点是无法利用多核。
1:1: 每个协程绑定一个线程。优点是并发性强,缺点是调度开销大。
M: 多个协程与多个线程之间的映射,结合了 N:1 和 1:1 的优点,但实现复杂。
d.Go 的 Goroutine
特点:
内存占用小(几 KB)。
由 Go 运行时调度,灵活高效。
c.GMP 模型的设计思想
a.GMP 模型
G (Goroutine): 运行的协程。
M (Machine): 操作系统线程。
P (Processor): 调度器,持有 G 的队列,运行时资源。
b.调度器流程
G: 需要执行的协程。
M: 执行 G 的线程。
P: 资源调度器,包含 G 的队列,分配给 M 执行 G。
c.调度流程
全局队列: 存储等待运行的 G。
P 的本地队列: 存储局部 G,优先使用。
调度过程:
M 从 P 的本地队列获取 G。
如果本地队列为空,尝试从全局队列或其他 P 的本地队列偷取 G。
G 执行后,M 再次获取下一个 G。
d.P 和 M 的数量
P 数量: 由环境变量 $GOMAXPROCS 决定,限制了并发的最大数量。
M 数量: 由运行时自动创建,最多可设置,但受限于系统资源。
e.调度策略
复用线程: 避免频繁创建和销毁线程。
work stealing: 当 P 的本地队列为空时,从其他 P 的队列偷取 G。
hand off: 当 M 因阻塞操作而无法继续工作时,释放 P 并创建新的 M。
抢占: 防止协程饿死,每个协程最多占用 CPU 一定时间(例如 10ms)。
d.Goroutine 调度流程
a.示例
创建: 使用 go func() 创建新协程,优先加入 P 的本地队列。
调度: M 从 P 的本地队列或全局队列中获取 G 并执行。
负载均衡: 当本地队列满时,部分 G 会被移到全局队列进行负载均衡。
work stealing: 从全局队列或其他 P 的队列中偷取 G。
b.调度器生命周期
特殊 M0 和 G0:
M0: 启动时的主线程,负责初始化和启动程序。
G0: 每个 M 的初始化协程,负责调度的初期操作。
c.调度器的调试
使用 go tool trace:
可视化程序的调度信息。
记录并分析调度流程。
使用 Debug trace:
输出调度器的状态信息,帮助调试。
1.2 反射1
00.汇总
反射是Go语言中的一个强大特性,它允许程序在运行时动态地检查和操作数据结构。
在Go语言中,反射主要用于处理接口和空接口interface{}。
通过反射,开发者可以在运行时获取类型信息、修改数据值,甚至调用函数。反射的实现依赖于Go语言的reflect包。
01.接口与反射的关系
Go语言中的接口本质上是一种数据结构,它分为两种类型:有方法集的接口和无方法集的接口。在Go的运行时环境中,有方法集的接口由iface结构体表示,无方法集的接口由eface结构体表示。
iface 结构体:表示有方法集的接口。
eface 结构体:表示没有方法集的接口,即空接口。
这两种结构体在reflect包中分别对应nonEmptyInterface和emptyInterface结构体。
02.反射的核心操作
反射的核心在于三个定律:
反射可以将interface{}类型变量转换为反射对象:通过reflect.TypeOf()和reflect.ValueOf()函数实现。
反射可以将反射对象还原成interface{}类型变量:通过Value.Interface()方法实现。
要修改反射对象,其值必须是可设置的:即该值必须是可取址的。
03.reflect.Type 与 reflect.Value
在Go语言的反射操作中,reflect.Type表示类型,reflect.Value表示值。reflect包提供了以下函数用于获取这些类型和值:
reflect.TypeOf(i any) Type:获取变量的类型。
reflect.ValueOf(i any) Value:获取变量的值。
这些函数的参数都是any类型(interface{}的别名),这说明反射操作与空接口密切相关。
04.使用 reflect.Type 获取类型信息
reflect.Type接口提供了许多方法来获取类型信息:
Kind():返回类型的基础类型(如int、string)。
Elem():返回指针、数组、切片、通道或映射的元素类型。
Size():返回类型的字节大小。
Comparable():判断类型是否可以比较。
Implements():判断类型是否实现了某个接口。
ConvertibleTo():判断类型是否可以转换为另一种类型。
05.使用 reflect.Value 操作值
reflect.Value结构体提供了一些方法来获取和修改值:
Type():获取值的类型。
Elem():获取指针、数组、切片、通道或映射的元素值。
Set():设置值,但前提是值必须是可设置的。
Interface():返回值的接口表示。
通过反射,您还可以获取函数的信息并调用函数,甚至可以访问和修改结构体字段和标签(Tag)。
06.结构体字段和标签操作
反射可以用于获取和修改结构体的字段和标签。通过reflect.Type可以获取结构体字段的类型信息,
而通过reflect.Value可以修改字段值。对于未导出的私有字段,可以使用一些特殊方法来访问和修改它们。
1.3 反射2
00.汇总
反射是一种在运行时检查语言自身结构的机制,它可以很灵活的去应对一些问题,但同时带来的弊端也很明显,
例如性能问题等等。在Go中,反射与interface{}密切相关,很大程度上,只要有interface{}出现的地方,就会有反射。
Go中的反射API是由标准库reflect包提供的。
01.接口
在开始之前先简单的了解一下位于runtime包下的两个接口。在Go中,接口本质上是结构体,
Go在运行时将接口分为了两大类,一类是没有方法集的接口,另一个类则是有方法集的接口。
对于含有方法集的接口来说,在运行时由如下的结构体iface来进行表示
type iface struct {
tab *itab // 包含 数据类型,接口类型,方法集等
data unsafe.Pointer // 指向值的指针
}
---------------------------------------------------------------------------------------------------------
而对于没有方法集接口来说,在运行时由eface 结构体来进行表示,如下
type eface struct {
_type *_type // 类型
data unsafe.Pointer // 指向值的指针
}
---------------------------------------------------------------------------------------------------------
而这两个结构体在reflect包下都有与其对应的结构体类型,iface对应的是nonEmptyInterface
type nonEmptyInterface struct {
itab *struct {
ityp *rtype // 静态接口类型
typ *rtype // 动态具体类型
hash uint32 // 类型哈希
_ [4]byte
fun [100000]unsafe.Pointer // 方法集
}
word unsafe.Pointer // 指向值的指针
}
---------------------------------------------------------------------------------------------------------
而eface对应的是emptyInterface
type emptyInterface struct {
typ *rtype // 动态具体类型
word unsafe.Pointer // 指向指针的值
}
---------------------------------------------------------------------------------------------------------
对于这两种类型,官方给出了很明确的定义
nonEmptyInterface: nonEmptyInterface is the header for an interface value with methods
emptyInterface:emptyInterface is the header for an interface{} value
上述提到了动态具体类型这一词,原文为dynamic concrete type,首先Go语言是一个百分之百的静态类型语言,静态这一词是体现在对外表现的抽象的接口类型是不变的,而动态表示是接口底层存储的具体实现的类型是可以变化的。至此,对于接口的简单原理只需要了解到这里就足够满足后续反射的学习。
02.桥梁
在reflect包下,分别有reflect.Type接口类型来表示Go中的类型,reflect.Value结构体类型来表示Go中的值
type Type interface {
...
Name() string
PkgPath() string
Size() uintptr
String() string
Kind() Kind
...
}
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
---------------------------------------------------------------------------------------------------------
上面的代码省略了很多细节,先只需要了解这两个类型的存在即可。Go中所有反射相关的操作都是基于这两个类型,
reflect包提供了两个函数来将Go中的类型转换为上述的两种类型以便进行反射操作,分别是reflect.TypeOf函数
func TypeOf(i any) Type
---------------------------------------------------------------------------------------------------------
与reflect.ValueOf函数
func ValueOf(i any) Value
---------------------------------------------------------------------------------------------------------
可以看到两个函数的参数类型都是any,也就是interface{}的别名。如果想要进行反射操作,
就需要先将其类型转换为interface{},这也是为什么前面提到了只要有反射就离不开空接口。
不严谨的说,空接口就是连接Go类型系统与反射的桥梁,下图很形象的描述了其过程。
03.核心
在Go中有三个经典的反射定律,结合上面所讲的内容也就非常好懂,分别如下
反射可以将interface{}类型变量转换成反射对象
反射可以将反射对象还原成interface{}类型变量
要修改反射对象,其值必须是可设置的
这三个定律便是Go反射的核心,当需要访问类型相关信息时,便需要用到reflect.TypeOf,当需要修改反射值时,就需要用到reflect.ValueOf
04.类型
a.介绍
reflect.Type代表着Go中的类型,使用reflect.TypeOf()函数可以将变量转换成reflect.Type。代码示例如下
func main() {
str := "hello world!"
reflectType := reflect.TypeOf(str)
fmt.Println(reflectType)
}
输出结果为
string
b.Kind
对于Type而言,Go内部使用reflect.Kind来表示Go中的基础类型,其本质上是无符号整型uint。
type Kind uint
-----------------------------------------------------------------------------------------------------
reflect包使用Kind枚举出了Go中所有的基础类型,如下所示
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Pointer
Slice
String
Struct
UnsafePointer
)
-----------------------------------------------------------------------------------------------------
Kind类型仅仅实现了Stringer接口的String()方法,该类型也仅有这一个方法,String()方法的返回值来自于一个其内部的map,如下所示
var kindNames = []string{
Invalid: "invalid",
Bool: "bool",
Int: "int",
Int8: "int8",
Int16: "int16",
Int32: "int32",
Int64: "int64",
Uint: "uint",
Uint8: "uint8",
Uint16: "uint16",
Uint32: "uint32",
Uint64: "uint64",
Uintptr: "uintptr",
Float32: "float32",
Float64: "float64",
Complex64: "complex64",
Complex128: "complex128",
Array: "array",
Chan: "chan",
Func: "func",
Interface: "interface",
Map: "map",
Pointer: "ptr",
Slice: "slice",
String: "string",
Struct: "struct",
UnsafePointer: "unsafe.Pointer",
}
type Type interface{
Kind() Kind
}
-----------------------------------------------------------------------------------------------------
通过Kind,可以知晓空接口存储的值究竟是什么基础类型,例如
func main() {
// 声明一个any类型的变量
var eface any
// 赋值
eface = 100
// 通过Kind方法,来获取其类型
fmt.Println(reflect.TypeOf(eface).Kind())
}
输出结果
int
c.Elem
type Type interface{
Elem() Type
}
使用Type.Elem()方法,可以判断类型为any的数据结构所存储的元素类型,可接收的底层参数类型必须是指针,切片,数组,通道,映射表其中之一,否则会panic。下面是代码示例
-----------------------------------------------------------------------------------------------------
func main() {
var eface any
eface = map[string]int{}
rType := reflect.TypeOf(eface)
// key()会返回map的键反射类型
fmt.Println(rType.Key().Kind())
fmt.Println(rType.Elem().Kind())
}
输出为
string
int
-----------------------------------------------------------------------------------------------------
指针也可以理解为是一个容器,对于指针使用Elem()会获得其指向元素的反射类型,代码示例如下
func main() {
var eface any
// 赋值指针
eface = new(strings.Builder)
rType := reflect.TypeOf(eface)
// 拿到指针所指向元素的反射类型
vType := rType.Elem()
// 输出包路径
fmt.Println(vType.PkgPath())
// 输出其名称
fmt.Println(vType.Name())
}
strings
Builder
-----------------------------------------------------------------------------------------------------
对于数组,切片,通道用使用起来都是类似的。
d.Size
type Type interface{
Size() uintptr
}
-----------------------------------------------------------------------------------------------------
通过Size方法可以获取对应类型所占的字节大小,示例如下
func main() {
fmt.Println(reflect.TypeOf(0).Size())
fmt.Println(reflect.TypeOf("").Size())
fmt.Println(reflect.TypeOf(complex(0, 0)).Size())
fmt.Println(reflect.TypeOf(0.1).Size())
fmt.Println(reflect.TypeOf([]string{}).Size())
}
输出结果为
8
16
16
8
24
提示:使用unsafe.Sizeof()可以达到同样的效果
e.Comparable
type Type interface{
Comparable() bool
}
通过Comparable方法可以判断一个类型是否可以被比较,例子如下
func main() {
fmt.Println(reflect.TypeOf("hello world!").Comparable())
fmt.Println(reflect.TypeOf(1024).Comparable())
fmt.Println(reflect.TypeOf([]int{}).Comparable())
fmt.Println(reflect.TypeOf(struct{}{}).Comparable())
}
输出如下
true
true
false
true
f.Implements
type Type interface{
Implements(u Type) bool
}
-----------------------------------------------------------------------------------------------------
通过Implements方法可以判断一个类型是否实现了某一接口
type MyInterface interface {
My() string
}
type MyStruct struct {
}
func (m MyStruct) My() string {
return "my"
}
type HisStruct struct {
}
func (h HisStruct) String() string {
return "his"
}
func main() {
rIface := reflect.TypeOf(new(MyInterface)).Elem()
fmt.Println(reflect.TypeOf(new(MyStruct)).Elem().Implements(rIface))
fmt.Println(reflect.TypeOf(new(HisStruct)).Elem().Implements(rIface))
}
输出结果
true
false
g.ConvertibleTo
type Type interface{
ConvertibleTo(u Type) bool
}
-----------------------------------------------------------------------------------------------------
使用ConvertibleTo方法可以判断一个类型是否可以被转换为另一个指定的类型
type MyInterface interface {
My() string
}
type MyStruct struct {
}
func (m MyStruct) My() string {
return "my"
}
type HisStruct struct {
}
func (h HisStruct) String() string {
return "his"
}
func main() {
rIface := reflect.TypeOf(new(MyInterface)).Elem()
fmt.Println(reflect.TypeOf(new(MyStruct)).Elem().ConvertibleTo(rIface))
fmt.Println(reflect.TypeOf(new(HisStruct)).Elem().ConvertibleTo(rIface))
}
输出
true
false
05.值
a.介绍
reflect.Value代表着反射接口的值,使用reflect.ValueOf()函数可以将变量转换成reflect.Value。代码示例如下
func main() {
str := "hello world!"
reflectValue := reflect.ValueOf(str)
fmt.Println(reflectValue)
}
输出结果为
hello world!
b.Type
func (v Value) Type() Type
Type方法可以获取一个反射值的类型
-----------------------------------------------------------------------------------------------------
func main() {
num := 114514
rValue := reflect.ValueOf(num)
fmt.Println(rValue.Type())
}
输出
int
c.Elem
func (v Value) Elem() Value
获取一个反射值的元素反射值
-----------------------------------------------------------------------------------------------------
func main() {
num := new(int)
*num = 114514
// 以指针为例子
rValue := reflect.ValueOf(num).Elem()
fmt.Println(rValue.Interface())
}
输出
114514
d.指针
获取一个反射值的指针方式有两种
// 返回一个表示v地址的指针反射值
func (v Value) Addr() Value
// 返回一个指向v的原始值的uinptr 等价于 uintptr(Value.Addr().UnsafePointer())
func (v Value) UnsafeAddr() uintptr
// 返回一个指向v的原始值的uintptr
// 仅当v的Kind为 Chan, Func, Map, Pointer, Slice, UnsafePointer时,否则会panic
func (v Value) Pointer() uintptr
// 返回一个指向v的原始值的unsafe.Pointer
// 仅当v的Kind为 Chan, Func, Map, Pointer, Slice, UnsafePointer时,否则会panic
func (v Value) UnsafePointer() unsafe.Pointer
-----------------------------------------------------------------------------------------------------
示例如下
func main() {
num := 1024
ele := reflect.ValueOf(&num).Elem()
fmt.Println("&num", &num)
fmt.Println("Addr", ele.Addr())
fmt.Println("UnsafeAddr", unsafe.Pointer(ele.UnsafeAddr()))
fmt.Println("Pointer", unsafe.Pointer(ele.Addr().Pointer()))
fmt.Println("UnsafePointer", ele.Addr().UnsafePointer())
}
输出
&num 0xc0000a6058
Addr 0xc0000a6058
UnsafeAddr 0xc0000a6058
Pointer 0xc0000a6058
UnsafePointer 0xc0000a6058
-----------------------------------------------------------------------------------------------------
提示
fmt.Println会反射获取参数的类型,如果是reflect.Value类型的话,会自动调用Value.Interface()来获取其原始值。
-----------------------------------------------------------------------------------------------------
换成一个map再来一遍
func main() {
dic := map[string]int{}
ele := reflect.ValueOf(&dic).Elem()
println(dic)
fmt.Println("Addr", ele.Addr())
fmt.Println("UnsafeAddr", *(*unsafe.Pointer)(unsafe.Pointer(ele.UnsafeAddr())))
fmt.Println("Pointer", unsafe.Pointer(ele.Pointer()))
fmt.Println("UnsafePointer", ele.UnsafePointer())
}
输出
0xc00010e4b0
Addr &map[]
UnsafeAddr 0xc00010e4b0
Pointer 0xc00010e4b0
UnsafePointer 0xc00010e4b0
e.设置值
func (v Value) Set(x Value)
倘若通过反射来修改反射值,那么其值必须是可取址的,这时应该通过指针来修改其元素值,而不是直接尝试修改元素的值。
-----------------------------------------------------------------------------------------------------
func main() {
// *int
num := new(int)
*num = 114514
rValue := reflect.ValueOf(num)
// 获取指针指向的元素
ele := rValue.Elem()
fmt.Println(ele.Interface())
ele.SetInt(11)
fmt.Println(ele.Interface())
}
输出如下
114514
11
f.获取值
func (v Value) Interface() (i any)
通过Interface()方法可以获取反射值原有的值
-----------------------------------------------------------------------------------------------------
func main() {
var str string
str = "hello"
rValue := reflect.ValueOf(str)
if v, ok := rValue.Interface().(string); ok {
fmt.Println(v)
}
}
输出
hello
06.函数
通过反射可以获取函数的一切信息,也可以反射调用函数
a.信息
通过反射类型来获取函数的一切信息
func Max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
rType := reflect.TypeOf(Max)
// 输出函数名称,字面量函数的类型没有名称
fmt.Println(rType.Name())
// 输出参数,返回值的数量
fmt.Println(rType.NumIn(), rType.NumOut())
rParamType := rType.In(0)
// 输出第一个参数的类型
fmt.Println(rParamType.Kind())
rResType := rType.Out(0)
// 输出第一个返回值的类型
fmt.Println(rResType.Kind())
}
输出
2 1
int
int
b.调用
通过反射值来调用函数
func (v Value) Call(in []Value) []Value
func main() {
// 获取函数的反射值
rType := reflect.ValueOf(Max)
// 传入参数数组
rResValue := rType.Call([]reflect.Value{reflect.ValueOf(18), reflect.ValueOf(50)})
for _, value := range rResValue {
fmt.Println(value.Interface())
}
}
输出
50
07.结构体
a.介绍
假设有如下结构体
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address string `json:"address"`
money int
}
func (p Person) Talk(msg string) string {
return msg
}
b.访问字段
reflect.StructField结构的结构如下
type StructField struct {
// 字段名称
Name string
// 包名
PkgPath string
// 类型名
Type Type
// Tag
Tag StructTag
// 字段的字节偏移
Offset uintptr
// 索引
Index []int
// 是否为嵌套字段
Anonymous bool
}
-----------------------------------------------------------------------------------------------------
访问结构体字段的方法有两种,一种是通过索引来进行访问,另一种是通过名称。
type Type interface{
Field(i int) StructField
}
-----------------------------------------------------------------------------------------------------
通过索引访问的例子如下
func main() {
rType := reflect.TypeOf(new(Person)).Elem()
// 输出结构体字段的数量
fmt.Println(rType.NumField())
for i := 0; i < rType.NumField(); i++ {
structField := rType.Field(i)
fmt.Println(structField.Index, structField.Name, structField.Type, structField.Offset, structField.IsExported())
}
}
输出
4
[0] Name string 0 true
[1] Age int 16 true
[2] Address string 24 true
[3] money int 40 false
-----------------------------------------------------------------------------------------------------
type Type interface{
FieldByName(name string) (StructField, bool)
}
通过名称访问的例子如下
func main() {
rType := reflect.TypeOf(new(Person)).Elem()
// 输出结构体字段的数量
fmt.Println(rType.NumField())
if field, ok := rType.FieldByName("money"); ok {
fmt.Println(field.Name, field.Type, field.IsExported())
}
}
输出
4
money int false
c.修改字段
倘若要修改结构体字段值,则必须传入一个结构体指针,下面是一个修改字段的例子
func main() {
// 传入指针
rValue := reflect.ValueOf(&Person{
Name: "",
Age: 0,
Address: "",
money: 0,
}).Elem()
// 获取字段
name := rValue.FieldByName("Name")
// 修改字段值
if (name != reflect.Value{}) { // 如果返回reflect.Value{},则说明该字段不存在
name.SetString("jack")
}
// 输出结构体
fmt.Println(rValue.Interface())
}
输出
{jack 0 0}
-----------------------------------------------------------------------------------------------------
对于修改结构体私有字段而言,需要进行一些额外的操作,如下
func main() {
// 传入指针
rValue := reflect.ValueOf(&Person{
Name: "",
Age: 0,
Address: "",
money: 0,
}).Elem()
// 获取一个私有字段
money := rValue.FieldByName("money")
// 修改字段值
if (money != reflect.Value{}) {
// 构造指向该结构体未导出字段的指针反射值
p := reflect.NewAt(money.Type(), money.Addr().UnsafePointer())
// 获取该指针所指向的元素,也就是要修改的字段
field := p.Elem()
// 修改值
field.SetInt(164)
}
// 输出结构体
fmt.Printf("%+v\n", rValue.Interface())
}
d.访问Tag
获取到StructField后,便可以直接访问其Tag
// 如果不存在,ok为false
func (tag StructTag) Lookup(key string) (value string, ok bool)
// 如果不存在,返回空字符串
func (tag StructTag) Get(key string) string
示例如下
func main() {
rType := reflect.TypeOf(new(Person)).Elem()
name, ok := rType.FieldByName("Name")
if ok {
fmt.Println(name.Tag.Lookup("json"))
fmt.Println(name.Tag.Get("json"))
}
}
输出
name true
name
e.访问方法
访问方法与访问字段的过程很相似,只是函数签名略有区别。reflect.Method结构体如下
type Method struct {
// 方法名
Name string
// 包名
PkgPath string
// 方法类型
Type Type
// 方法对应的函数,第一个参数是接收者
Func Value
// 索引
Index int
}
-----------------------------------------------------------------------------------------------------
访问方法信息示例如下
func main() {
// 获取结构体反射类型
rType := reflect.TypeOf(new(Person)).Elem()
// 输出方法个数
fmt.Println(rType.NumMethod())
// 遍历输出方法信息
for i := 0; i < rType.NumMethod(); i++ {
method := rType.Method(i)
fmt.Println(method.Index, method.Name, method.Type, method.IsExported())
}
}
输出
1
0 Talk func(main.Person, string) string true
-----------------------------------------------------------------------------------------------------
如果想要获取方法的参数和返回值细节,可以通过Method.Func来进行获取,过程与访问函数信息一致,将上面的代码稍微修改下
func main() {
// 获取结构体反射类型
rType := reflect.TypeOf(new(Person)).Elem()
// 输出方法个数
fmt.Println(rType.NumMethod())
// 遍历输出方法信息
for i := 0; i < rType.NumMethod(); i++ {
method := rType.Method(i)
fmt.Println(method.Index, method.Name, method.Type, method.IsExported())
fmt.Println("方法参数")
for i := 0; i < method.Func.Type().NumIn(); i++ {
fmt.Println(method.Func.Type().In(i).String())
}
fmt.Println("方法返回值")
for i := 0; i < method.Func.Type().NumOut(); i++ {
fmt.Println(method.Func.Type().Out(i).String())
}
}
}
-----------------------------------------------------------------------------------------------------
可以看到第一个参数是main.Person,也就是接收者类型
1
0 Talk func(main.Person, string) string true
方法参数
main.Person
string
方法返回值
string
f.调用方法
调用方法与调用函数的过程相似,而且并不需要手动传入接收者,例子如下
func main() {
// 获取结构体反射类型
rValue := reflect.ValueOf(new(Person)).Elem()
// 输出方法个数
fmt.Println(rValue.NumMethod())
// 遍历输出方法信息
talk := rValue.MethodByName("Talk")
if (talk != reflect.Value{}) {
// 调用方法,并获取返回值
res := talk.Call([]reflect.Value{reflect.ValueOf("hello,reflect!")})
// 遍历输出返回值
for _, re := range res {
fmt.Println(re.Interface())
}
}
}
输出
1
hello,reflect!
08.创建
通过反射可以构造新的值,reflect包同时根据一些特殊的类型提供了不同的更为方便的函数。
a.基本类型
// 返回指向反射值的指针反射值
func New(typ Type) Value
-----------------------------------------------------------------------------------------------------
以string为例
func main() {
rValue := reflect.New(reflect.TypeOf(*new(string)))
rValue.Elem().SetString("hello world!")
fmt.Println(rValue.Elem().Interface())
}
hello world!
b.结构体
结构体的创建同样用到reflect.New函数
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address string `json:"address"`
money int
}
func (p Person) Talk(msg string) string {
return msg
}
func main() {
// 创建结构体反射值
rType := reflect.TypeOf(new(Person)).Elem()
person := reflect.New(rType).Elem()
fmt.Println(person.Interface())
}
输出
{ 0 0}
c.切片
反射创建切片
func MakeSlice(typ Type, len, cap int) Value
func main() {
// 创建切片反射值
rValue := reflect.MakeSlice(reflect.TypeOf(*new([]int)), 10, 10)
// 遍历赋值
for i := 0; i < 10; i++ {
rValue.Index(i).SetInt(int64(i))
}
fmt.Println(rValue.Interface())
}
[0 1 2 3 4 5 6 7 8 9]
d.Map
反射创建Map
func MakeMapWithSize(typ Type, n int) Value
func main() {
//构建map反射值
rValue := reflect.MakeMapWithSize(reflect.TypeOf(*new(map[string]int)), 10)
// 设置值
rValue.SetMapIndex(reflect.ValueOf("a"), reflect.ValueOf(1))
fmt.Println(rValue.Interface())
}
map[a:1]
e.管道
反射创建管道
func MakeChan(typ Type, buffer int) Value
func main() {
// 创建管道反射值
makeChan := reflect.MakeChan(reflect.TypeOf(new(chan int)).Elem(), 0)
fmt.Println(makeChan.Interface())
}
f.函数
反射创建函数
func MakeFunc(typ Type, fn func(args []Value) (results []Value)) Value
func main() {
// 传入包装类型和函数体
fn := reflect.MakeFunc(reflect.TypeOf(new(func(int))).Elem(), func(args []reflect.Value) (results []reflect.Value) {
for _, arg := range args {
fmt.Println(arg.Interface())
}
return nil
})
fmt.Println(fn.Type())
fn.Call([]reflect.Value{reflect.ValueOf(1024)})
}
输出
func(int)
1024
09.完全相等
reflect.DeepEqual是反射包下提供的一个用于判断两个变量是否完全相等的函数,签名如下。
func DeepEqual(x, y any) bool
---------------------------------------------------------------------------------------------------------
该函数对于每一种基础类型都做了处理,下面是一些类型判断方式。
数组:数组中的每一个元素都完全相等
切片:都为nil时,判为完全相等,或者都不为空时,长度范围内的元素完全相等
结构体:所有字段都完全相等
映射表:都为nil时,为完全相等,都不为nil时,每一个键所映射的值都完全相等
指针:指向同一个元素或指向的元素完全相等
接口:接口的具体类型完全相等时
函数:只有两者都为nil时才是完全相等,否则就不是完全相等
---------------------------------------------------------------------------------------------------------
下面是一些例子:
切片
func main() {
a := make([]int, 100)
b := make([]int, 100)
fmt.Println(reflect.DeepEqual(a, b))
}
输出
true
---------------------------------------------------------------------------------------------------------
结构体
func main() {
mike := Person{
Name: "mike",
Age: 39,
Father: nil,
}
jack := Person{
Name: "jack",
Age: 18,
Father: &mike,
}
tom := Person{
Name: "tom",
Age: 18,
Father: &mike,
}
fmt.Println(reflect.DeepEqual(mike, jack))
fmt.Println(reflect.DeepEqual(tom, jack))
fmt.Println(reflect.DeepEqual(jack, jack))
}
输出
false
false
true
1.4 IO操作
00.汇总
Go语言提供文件处理的标准库大致以下几个:
os包,负责与不同OS文件系统交互的具体实现
io,读写IO的抽象层
fs,文件系统的抽象层
01.打开
常见的两种打开文件的方式是使用os包提供的两个函数,Open函数返回值一个文件指针和一个错误,
func Open(name string) (*File, error)
---------------------------------------------------------------------------------------------------------
后者OpenFile能够提供更加细粒度的控制,实际上Open函数就是对OpenFile函数的一个简单封装。
func OpenFile(name string, flag int, perm FileMode) (*File, error)
---------------------------------------------------------------------------------------------------------
先来介绍第一种使用方法,直接提供对应的文件名即可,代码如下
func main() {
file, err := os.Open("README.txt")
fmt.Println(file, err)
}
---------------------------------------------------------------------------------------------------------
文件的查找路径默认为项目go.mod文件所在的路径,由于项目下并没有文件README.txt,所以自然会返回一个错误。
<nil> open README.txt: The system cannot find the file specified.
---------------------------------------------------------------------------------------------------------
因为IO错误的类型有很多,所以需要手动的去判断文件是否存在,同样的os包也为此提供了方便函数,修改后的代码如下
func main() {
file, err := os.Open("README.txt")
if os.IsNotExist(err) {
fmt.Println("文件不存在")
} else if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件读取成功", file)
}
}
再次运行输出如下
文件不存在
---------------------------------------------------------------------------------------------------------
事实上第一种函数读取的文件仅仅只是只读的,无法被修改,Open函数也只是调用了OpenFile而已
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
通过OpenFile函数可以控制更多细节,例如修改文件描述符和文件权限,关于文件描述符,os包下提供了以下常量以供使用。
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 // 以同步IO的方式打开文件
O_TRUNC int = syscall.O_TRUNC // 当打开的时候截断可写的文件
)
---------------------------------------------------------------------------------------------------------
关于文件权限的则提供了以下常量。
const (
ModeDir = fs.ModeDir // d: 目录
ModeAppend = fs.ModeAppend // a: 只能添加
ModeExclusive = fs.ModeExclusive // l: 专用
ModeTemporary = fs.ModeTemporary // T: 临时文件
ModeSymlink = fs.ModeSymlink // L: 符号链接
ModeDevice = fs.ModeDevice // D: 设备文件
ModeNamedPipe = fs.ModeNamedPipe // p: 具名管道 (FIFO)
ModeSocket = fs.ModeSocket // S: Unix 域套接字
ModeSetuid = fs.ModeSetuid // u: setuid
ModeSetgid = fs.ModeSetgid // g: setgid
ModeCharDevice = fs.ModeCharDevice // c: Unix 字符设备, 前提是设置了 ModeDevice
ModeSticky = fs.ModeSticky // t: 黏滞位
ModeIrregular = fs.ModeIrregular // ?: 非常规文件
// 类型位的掩码. 对于常规文件而言,什么都不会设置.
ModeType = fs.ModeType
ModePerm = fs.ModePerm // Unix 权限位, 0o777
)
---------------------------------------------------------------------------------------------------------
提示:truncates意思即为将文件容量调整到合适的大小以容纳数据,不大也不小。
---------------------------------------------------------------------------------------------------------
下面是一个以读写模式打开一个文件的代码例子,权限为0666,表示为所有人都可以对该文件进行读写,且不存在时会自动创建。
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if os.IsNotExist(err) {
fmt.Println("文件不存在")
} else if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件打开成功", file.Name())
file.Close()
}
}
输出如下
文件打开成功 README.txt
---------------------------------------------------------------------------------------------------------
倘若只是想获取该文件的一些信息,并不想读取该文件,可以使用os.Stat()函数进行操作,代码示例如下
func main() {
fileInfo, err := os.Stat("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(fmt.Sprintf("%+v", fileInfo))
}
}
输出如下
&{name:README.txt FileAttributes:32 CreationTime:{LowDateTime:3603459389 HighDateTime:31016791} LastAccessTime:{LowDateTime:3603459389 HighDateTime:31016791} LastWriteTime:{LowDateTime:3603459389 HighDateTime:31016791} FileSizeHigh
:0 FileSizeLow:0 Reserved0:0 filetype:0 Mutex:{state:0 sema:0} path:README.txt vol:0 idxhi:0 idxlo:0 appendNameToPath:false}
---------------------------------------------------------------------------------------------------------
注意:打开一个文件后永远要记得关闭该文件,通常关闭操作会放在defer语句里
defer file.Close()
02.读取
a.介绍
当成功的打开文件后,便可以进行读取操作了,关于读取文件的操作,*os.File类型提供了以下几个公开的方法
// 将文件读进传入的字节切片
func (f *File) Read(b []byte) (n int, err error)
// 相较于第一种可以从指定偏移量读取
func (f *File) ReadAt(b []byte, off int64) (n int, err error)
b.大多数情况第一种使用的较多。针对于第一种方法,需要自行编写逻辑来进行读取时切片的动态扩容,代码如下
func ReadFile(file *os.File) ([]byte, error) {
buffer := make([]byte, 0, 512)
for {
// 当容量不足时
if len(buffer) == cap(buffer) {
// 扩容
buffer = append(buffer, 0)[:len(buffer)]
}
// 继续读取文件
offset, err := file.Read(buffer[len(buffer):cap(buffer)])
// 将已写入的数据归入切片
buffer = buffer[:len(buffer)+offset]
// 发生错误时
if err != nil {
if errors.Is(err, io.EOF) {
err = nil
}
return buffer, err
}
}
}
剩余逻辑如下
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件打开成功", file.Name())
bytes, err := ReadFile(file)
if err != nil {
fmt.Println("文件读取异常", err)
} else {
fmt.Println(string(bytes))
}
file.Close()
}
}
输出为
文件打开成功 README.txt
hello world!
c.os.ReadFile
除此之外,还可以使用两个方便函数来进行文件读取,分别是os包下的ReadFile函数,以及io包下的ReadAll函数。
对于os.ReadFile而言,只需要提供文件路径即可,而对于io.ReadAll而言,则需要提供一个io.Reader类型的实现,
-----------------------------------------------------------------------------------------------------
os.ReadFile
func ReadFile(name string) ([]byte, error)
-----------------------------------------------------------------------------------------------------
使用例子如下
func main() {
bytes, err := os.ReadFile("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(bytes))
}
}
输出如下
hello world!
d.io.ReadAll
io.ReadAll
func ReadAll(r Reader) ([]byte, error)
使用例子如下
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件打开成功", file.Name())
bytes, err := io.ReadAll(file)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(bytes))
}
file.Close()
}
}
文件打开成功 README.txt
hello world!
03.写入
a.介绍
os.File结构体提供了以下几种方法以供写入数据
// 写入字节切片
func (f *File) Write(b []byte) (n int, err error)
// 写入字符串
func (f *File) WriteString(s string) (n int, err error)
// 从指定位置开始写,当以os.O_APPEND模式打开时,会返回错误
func (f *File) WriteAt(b []byte, off int64) (n int, err error)
b.示例
如果想要对一个文件写入数据,则必须以O_WRONLY或O_RDWR的模式打开,否则无法成功写入文件。
下面是一个以os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC模式打开文件,且权限为0666向指定写入数据的例子
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件打开成功", file.Name())
for i := 0; i < 5; i++ {
offset, err := file.WriteString("hello world!\n")
if err != nil {
fmt.Println(offset, err)
}
}
fmt.Println(file.Close())
}
}
-----------------------------------------------------------------------------------------------------
由于是以os.O_APPEND模式打开的文件,所以在写入文件时会将数据添加到文件尾部,执行完毕后文件内容如下
hello world!
hello world!
hello world!
hello world!
hello world!
-----------------------------------------------------------------------------------------------------
向文件写入字节切片也是类似的操作,就不再赘述。对于写入文件的操作标准库同样提供了方便函数,分别是os.WriteFile与io.WriteString
os.WriteFile
func WriteFile(name string, data []byte, perm FileMode) error
-----------------------------------------------------------------------------------------------------
使用例子如下
func main() {
err := os.WriteFile("README.txt", []byte("hello world!\n"), 0666)
if err != nil {
fmt.Println(err)
}
}
此时文件内容如下
hello world!
c.io.WriteString
func WriteString(w Writer, s string) (n int, err error)
使用例子如下
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件打开成功", file.Name())
for i := 0; i < 5; i++ {
offset, err := io.WriteString(file, "hello world!\n")
if err != nil {
fmt.Println(offset, err)
}
}
fmt.Println(file.Close())
}
}
hello world!
hello world!
hello world!
hello world!
hello world!
d.提示
os.Create函数用于创建文件,本质上也是对OpenFile的封装。
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
注意:在创建一个文件时,如果其父目录不存在,将创建失败并会返回错误。
04.复制
a.对于复制文件而言,需要同时打开两个文件,第一种方法是将原文件中的数据读取出来,然后写入目标文件中,代码示例如下
func main() {
// 从原文件中读取数据
data, err := os.ReadFile("README.txt")
if err != nil {
fmt.Println(err)
return
}
// 写入目标文件
err = os.WriteFile("README(1).txt", data, 0666)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("复制成功")
}
}
b.*os.File.ReadFrom
另一种方法是使用os.File提供的方法ReadFrom,打开文件时,一个只读,一个只写。
func (f *File) ReadFrom(r io.Reader) (n int64, err error)
使用示例如下
func main() {
// 以只读的方式打开原文件
origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
if err != nil {
fmt.Println(err)
return
}
defer origin.Close()
// 以只写的方式打开副本文件
target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
fmt.Println(err)
return
}
defer target.Close()
// 从原文件中读取数据,然后写入副本文件
offset, err := target.ReadFrom(origin)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("文件复制成功", offset)
}
c.io.Copy
还有一种方法就是使用io.Copy方便函数
func Copy(dst Writer, src Reader) (written int64, err error)
使用示例如下
func main() {
// 以只读的方式打开原文件
origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
if err != nil {
fmt.Println(err)
return
}
defer origin.Close()
// 以只写的方式打开副本文件
target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
fmt.Println(err)
return
}
defer target.Close()
// 复制
written, err := io.Copy(target, origin)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(written)
}
}
05.重命名
重命名也可以理解为移动文件,会用到os包下的Rename函数。
func Rename(oldpath, newpath string) error
示例如下
func main() {
err := os.Rename("README.txt", "readme.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("重命名成功")
}
}
06.删除
删除操作相较于其他操作要简单的多,只会用到os包下的两个函数
// 删除单个文件或者空目录,当目录不为空时会返回错误
func Remove(name string) error
// 删除指定目录的所有文件和目录包括子目录与子文件
func RemoveAll(path string) error
使用起来十分的简单,下面是删除目录的例子
func main() {
// 删除当前目录下所有的文件与子目录
err := os.RemoveAll(".")
if err != nil {
fmt.Println(err)
}else {
fmt.Println("删除成功")
}
}
---------------------------------------------------------------------------------------------------------
下面删除单个文件的例子
func main() {
// 删除当前目录下所有的文件与子目录
err := os.Remove("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("删除成功")
}
}
07.刷盘
os.Sync这一个函数封装了底层的系统调用Fsync,用于将操作系统中缓存的IO写入落实到磁盘上。
func main() {
create, err := os.Create("test.txt")
if err != nil {
panic(err)
}
defer create.Close()
_, err = create.Write([]byte("hello"))
if err != nil {
panic(err)
}
// 刷盘
if err := create.Sync();err != nil {
return
}
}
08.文件夹
a.读取
a.os.ReadDir
第一种方式是使用os.ReadDir函数
func ReadDir(name string) ([]DirEntry, error)
func main() {
// 当前目录
dir, err := os.ReadDir(".")
if err != nil {
fmt.Println(err)
} else {
for _, entry := range dir {
fmt.Println(entry.Name())
}
}
}
b.os.File.ReadDir
第二种方式是使用*os.File.ReadDir函数,os.ReadDir本质上也只是对*os.File.ReadDir的一层简单封装。
// n < 0时,则读取文件夹下所有的内容
func (f *File) ReadDir(n int) ([]DirEntry, error)
func main() {
// 当前目录
dir, err := os.Open(".")
if err != nil {
fmt.Println(err)
}
defer dir.Close()
dirs, err := dir.ReadDir(-1)
if err != nil {
fmt.Println(err)
} else {
for _, entry := range dirs {
fmt.Println(entry.Name())
}
}
}
b.创建
创建文件夹操作会用到os包下的两个函数
// 用指定的权限创建指定名称的目录
func Mkdir(name string, perm FileMode) error
// 相较于前者该函数会创建一切必要的父目录
func MkdirAll(path string, perm FileMode) error
示例如下
func main() {
err := os.Mkdir("src", 0666)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("创建成功")
}
}
c.复制
我们可以自己写函数递归遍历整个文件夹,不过filepath标准库已经提供了类似功能的函数,所以可以直接使用,一个简单的文件夹复制的代码示例如下。
func CopyDir(src, dst string) error {
// 检查源文件夹的状态
_, err := os.Stat(src)
if err != nil {
return err
}
return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
// 计算相对路径
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
// 拼接目标路径
destpath := filepath.Join(dst, rel)
// 创建文件夹
var dirpath string
var mode os.FileMode = 0755
if info.IsDir() {
dirpath = destpath
mode = info.Mode()
} else if info.Mode().IsRegular() {
dirpath = filepath.Dir(destpath)
}
if err := os.MkdirAll(dirpath, mode); err != nil {
return err
}
// 创建文件
if info.Mode().IsRegular() {
srcfile, err := os.Open(path)
if err != nil {
return err
}
// 一定要记得关闭文件
defer srcfile.Close()
destfile, err := os.OpenFile(destpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer destfile.Close()
// 复制文件内容
if _, err := io.Copy(destfile, srcfile); err != nil {
return err
}
return nil
}
return nil
})
}
filepath.Walk会递归遍历整个文件夹,在过程中,遇到文件夹就创建文件夹,遇到文件就创建新文件并复制,代码相比复制文件有点多但算不上复杂。
1.5 协程
01.介绍
协程(coroutine)是一种轻量级的线程,或者说是用户态的线程,不受操作系统直接调度,
由Go语言自身的调度器进行运行时调度,因此上下文切换开销非常小,这也是为什么Go的并发性能很不错的原因之一。
协程这一概念并非Go首次提出,Go也不是第一个支持协程的语言,但Go是第一个能够将协程和并发支持的相当简洁和优雅的语言。
02.示例
提示:具有返回值的内置函数不允许跟随在go关键字后面,例如下面的错误示范
go make([]int,10) // go discards result of make([]int, 10) (value of type []int)
---------------------------------------------------------------------------------------------------------
func main() {
go fmt.Println("hello world!")
go hello()
go func() {
fmt.Println("hello world!")
}()
}
func hello() {
fmt.Println("hello world!")
}
以上三种开启协程的方式都是可以的,但是其实这个例子执行过后在大部分情况下什么都不会输出,协程是并发执行的,系统创建协程需要时间,而在此之前,主协程早已运行结束,一旦主线程退出,其他子协程也就自然退出了。并且协程的执行顺序也是不确定的,无法预判的,例如下面的例子
---------------------------------------------------------------------------------------------------------
func main() {
fmt.Println("start")
for i := 0; i < 10; i++ {
go fmt.Println(i)
}
fmt.Println("end")
}
这是一个在循环体中开启协程的例子,永远也无法精准的预判到它到底会输出什么。可能子协程还没开始运行,主协程就已经结束了,情况如下
start
end
---------------------------------------------------------------------------------------------------------
又或者只有一部分子协程在主协程退出前成功运行,情况如下
start
0
1
5
3
4
6
7
end
---------------------------------------------------------------------------------------------------------
最简单的做法就是让主协程等一会儿,需要使用到time包下的Sleep函数,可以使当前协程暂停一段时间,例子如下
func main() {
fmt.Println("start")
for i := 0; i < 10; i++ {
go fmt.Println(i)
}
// 暂停1ms
time.Sleep(time.Millisecond)
fmt.Println("end")
}
---------------------------------------------------------------------------------------------------------
再次执行输出如下,可以看到所有的数字都完整输出了,没有遗漏
start
0
1
5
2
3
4
6
8
9
7
end
---------------------------------------------------------------------------------------------------------
但是顺序还是乱的,因此让每次循环都稍微的等一下。例子如下
func main() {
fmt.Println("start")
for i := 0; i < 10; i++ {
go fmt.Println(i)
time.Sleep(time.Millisecond)
}
time.Sleep(time.Millisecond)
fmt.Println("end")
}
现在的输出已经是正常的顺序了
start
0
1
2
3
4
5
6
7
8
9
end
上面的例子中结果输出很完美,那么并发的问题解决了吗,不,一点也没有。对于并发的程序而言,不可控的因素非常多,执行的时机,先后顺序,执行过程的耗时等等,倘若循环中子协程的工作不只是一个简单的输出数字,而是一个非常巨大复杂的任务,耗时的不确定的,那么依旧会重现之前的问题。例如下方代码
---------------------------------------------------------------------------------------------------------
func main() {
fmt.Println("start")
for i := 0; i < 10; i++ {
go hello(i)
time.Sleep(time.Millisecond)
}
time.Sleep(time.Millisecond)
fmt.Println("end")
}
func hello(i int) {
// 模拟随机耗时
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
fmt.Println(i)
}
这段代码的输出依旧是不确定的,下面是可能的情况之一
start
0
3
4
end
---------------------------------------------------------------------------------------------------------
因此time.Sleep并不是一种良好的解决办法,幸运的是Go提供了非常多的并发控制手段,常用的并发控制方法有三种:
channel:管道
WaitGroup:信号量
Context:上下文
---------------------------------------------------------------------------------------------------------
三种方法有着不同的适用情况,WaitGroup可以动态的控制一组指定数量的协程,Context更适合子孙协程嵌套层级更深的情况,管道更适合协程间通信。对于较为传统的锁控制,Go也对此提供了支持:
Mutex:互斥锁
RWMutex :读写互斥锁
1.6 通道
00.介绍
channel,译为管道,Go对于管道的作用如下解释:
Do not communicate by sharing memory; instead, share memory by communicating.
即通过消息来进行内存共享,channel就是为此而生,它是一种在协程间通信的解决方案,同时也可以用于并发控制,先来认识下channel的基本语法。Go中通过关键字chan来代表管道类型,同时也必须声明管道的存储类型,来指定其存储的数据是什么类型,下面的例子是一个普通管道的模样。
var ch chan int
这是一个管道的声明语句,此时管道还未初始化,其值为nil,不可以直接使用。
01.创建
在创建管道时,有且只有一种方法,那就是使用内置函数make,对于管道而言,make函数接收两个参数,第一个是管道的类型,第二个是可选参数为管道的缓冲大小。例子如下
intCh := make(chan int)
// 缓冲区大小为1的管道
strCh := make(chan string, 1)
---------------------------------------------------------------------------------------------------------
在使用完一个管道后一定要记得关闭该管道,使用内置函数close来关闭一个管道,该函数签名如下。
func close(c chan<- Type)
---------------------------------------------------------------------------------------------------------
一个关闭管道的例子如下
func main() {
intCh := make(chan int)
// do something
close(intCh)
}
有些时候使用defer来关闭管道可能会更好。
02.读写
对于一个管道而言,Go使用了两种很形象的操作符来表示读写操作:
ch <-:表示对一个管道写入数据
<- ch:表示对一个管道读取数据
---------------------------------------------------------------------------------------------------------
<-很生动的表示了数据的流动方向,来看一个对int类型的管道读写的例子
func main() {
// 如果没有缓冲区则会导致死锁
intCh := make(chan int, 1)
defer close(intCh)
// 写入数据
intCh <- 114514
// 读取数据
fmt.Println(<-intCh)
}
上面的例子中创建了一个缓冲区大小为1的int型管道,对其写入数据114514,然后再读取数据并输出,最后关闭该管道。对于读取操作而言,还有第二个返回值,一个布尔类型的值,用于表示数据是否读取成功
---------------------------------------------------------------------------------------------------------
ints, ok := <-intCh
管道中的数据流动方式与队列一样,即先进先出(FIFO),协程对于管道的操作是同步的,在某一个时刻,只有一个协程能够对其写入数据,同时也只有一个协程能够读取管道中的数据。
03.无缓冲
对于无缓冲管道而言,因为缓冲区容量为0,所以不会临时存放任何数据。正因为无缓冲管道无法存放数据,在向管道写入数据时必须立刻有其他协程来读取数据,否则就会阻塞等待,读取数据时也是同理,这也解释了为什么下面看起来很正常的代码会发生死锁。
func main() {
// 创建无缓冲管道
ch := make(chan int)
defer close(ch)
// 写入数据
ch <- 123
// 读取数据
n := <-ch
fmt.Println(n)
}
---------------------------------------------------------------------------------------------------------
无缓冲管道不应该同步的使用,正确来说应该开启一个新的协程来发送数据,如下例
func main() {
// 创建无缓冲管道
ch := make(chan int)
defer close(ch)
go func() {
// 写入数据
ch <- 123
}()
// 读取数据
n := <-ch
fmt.Println(n)
}
04.有缓冲
当管道有了缓冲区,就像是一个阻塞队列一样,读取空的管道和写入已满的管道都会造成阻塞。
无缓冲管道在发送数据时,必须立刻有人接收,否则就会一直阻塞。对于有缓冲管道则不必如此,
对于有缓冲管道写入数据时,会先将数据放入缓冲区里,只有当缓冲区容量满了才会阻塞的等待协程来读取管道中的数据。
同样的,读取有缓冲管道时,会先从缓冲区中读取数据,直到缓冲区没数据了,才会阻塞的等待协程来向管道中写入数据。
因此,无缓冲管道中会造成死锁例子在这里可以顺利运行。
---------------------------------------------------------------------------------------------------------
func main() {
// 创建有缓冲管道
ch := make(chan int, 1)
defer close(ch)
// 写入数据
ch <- 123
// 读取数据
n := <-ch
fmt.Println(n)
}
---------------------------------------------------------------------------------------------------------
尽管可以顺利运行,但这种同步读写的方式是非常危险的,一旦管道缓冲区空了或者满了,将会永远阻塞下去,
因为没有其他协程来向管道中写入或读取数据。来看看下面的一个例子
func main() {
// 创建有缓冲管道
ch := make(chan int, 5)
// 创建两个无缓冲管道
chW := make(chan struct{})
chR := make(chan struct{})
defer func() {
close(ch)
close(chW)
close(chR)
}()
// 负责写
go func() {
for i := 0; i < 10; i++ {
ch <- i
fmt.Println("写入", i)
}
chW <- struct{}{}
}()
// 负责读
go func() {
for i := 0; i < 10; i++ {
// 每次读取数据都需要花费1毫秒
time.Sleep(time.Millisecond)
fmt.Println("读取", <-ch)
}
chR <- struct{}{}
}()
fmt.Println("写入完毕", <-chW)
fmt.Println("读取完毕", <-chR)
}
---------------------------------------------------------------------------------------------------------
这里总共创建了3个管道,一个有缓冲管道用于协程间通信,两个无缓冲管道用于同步父子协程的执行顺序。
负责读的协程每次读取之前都会等待1毫秒,负责写的协程一口气做多也只能写入5个数据,
因为管道缓冲区最大只有5,在没有协程来读取之前,只能阻塞等待。所以该示例输出如下
写入 0
写入 1
写入 2
写入 3
写入 4 // 一下写了5个,缓冲区满了,等其他协程来读
读取 0
写入 5 // 读一个,写一个
读取 1
写入 6
读取 2
写入 7
读取 3
写入 8
写入 9
读取 4
写入完毕 {} // 所有的数据都发送完毕,写协程执行完毕
读取 5
读取 6
读取 7
读取 8
读取 9
读取完毕 {} // 所有的数据都读完了,读协程执行完毕
---------------------------------------------------------------------------------------------------------
可以看到负责写的协程刚开始就一口气发送了5个数据,缓冲区满了以后就开始阻塞等待读协程来读取,
后面就是每当读协程1毫秒读取一个数据,缓冲区有空位了,写协程就写入一个数据,
直到所有数据发送完毕,写协程执行结束,随后当读协程将缓冲区所有数据读取完毕后,读协程也执行结束,最后主协程退出。
---------------------------------------------------------------------------------------------------------
提示
通过内置函数len可以访问管道缓冲区中数据的个数,通过cap可以访问管道缓冲区的大小。
func main() {
ch := make(chan int, 5)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(len(ch), cap(ch))
}
输出
3 5
---------------------------------------------------------------------------------------------------------
利用管道的阻塞条件,可以很轻易的写出一个主协程等待子协程执行完毕的例子
func main() {
// 创建一个无缓冲管道
ch := make(chan struct{})
defer close(ch)
go func() {
fmt.Println(2)
// 写入
ch <- struct{}{}
}()
// 阻塞等待读取
<-ch
fmt.Println(1)
}
输出
2
1
---------------------------------------------------------------------------------------------------------
通过有缓冲管道还可以实现一个简单的互斥锁,看下面的例子
var count = 0
// 缓冲区大小为1的管道
var lock = make(chan struct{}, 1)
func Add() {
// 加锁
lock <- struct{}{}
fmt.Println("当前计数为", count, "执行加法")
count += 1
// 解锁
<-lock
}
func Sub() {
// 加锁
lock <- struct{}{}
fmt.Println("当前计数为", count, "执行减法")
count -= 1
// 解锁
<-lock
}
由于管道的缓冲区大小为1,最多只有一个数据存放在缓冲区中。Add和Sub函数在每次操作前都会尝试向管道中发送数据,
由于缓冲区大小为1,倘若有其他协程已经写入了数据,缓冲区已经满了,当前协程就必须阻塞等待,直到缓冲区空出位置来,
如此一来,在某一个时刻,最多只能有一个协程对变量count进行修改,这样就实现了一个简单的互斥锁。
05.注意点
a.以下几种情况使用不当会导致管道阻塞:
a.读写无缓冲管道
当对一个无缓冲管道直接进行同步读写操作都会导致该协程阻塞
func main() {
// 创建了一个无缓冲管道
intCh := make(chan int)
defer close(intCh)
// 发送数据
intCh <- 1
// 读取数据
ints, ok := <-intCh
fmt.Println(ints, ok)
}
b.读取空缓冲区的管道
当读取一个缓冲区为空的管道时,会导致该协程阻塞
func main() {
// 创建的有缓冲管道
intCh := make(chan int, 1)
defer close(intCh)
// 缓冲区为空,阻塞等待其他协程写入数据
ints, ok := <-intCh
fmt.Println(ints, ok)
}
c.写入满缓冲区的管道
当管道的缓冲区已满,对其写入数据会导致该协程阻塞
func main() {
// 创建的有缓冲管道
intCh := make(chan int, 1)
defer close(intCh)
intCh <- 1
// 满了,阻塞等待其他协程来读取数据
intCh <- 1
}
d.管道为ni
当管道为nil时,无论怎样读写都会导致当前协程阻塞
func main() {
var intCh chan int
// 写
intCh <- 1
}
func main() {
var intCh chan int
// 读
fmt.Println(<-intCh)
}
b.以下几种情况还会导致panic:
a.关闭一个nil管道
当管道为nil时,使用close函数对其进行关闭操作会导致panic`
func main() {
var intCh chan int
close(intCh)
}
b.写入已关闭的管道
对一个已关闭的管道写入数据会导致panic
func main() {
intCh := make(chan int, 1)
close(intCh)
intCh <- 1
}
c.关闭已关闭的管道
在一些情况中,管道可能经过层层传递,调用者或许也不知道到底该由谁来关闭管道,
如此一来,可能会发生关闭一个已经关闭了的管道,就会发生panic。
func main() {
ch := make(chan int, 1)
defer close(ch)
go write(ch)
fmt.Println(<-ch)
}
func write(ch chan<- int) {
// 只能对管道发送数据
ch <- 1
close(ch)
}
06.单向管道
双向管道指的是既可以写,也可以读,即可以在管道两边进行操作。
单向管道指的是只读或只写的管道,即只能在管道的一边进行操作。
手动创建的一个只读或只写的管道没有什么太大的意义,因为不能对管道读写就失去了其存在的作用。
单向管道通常是用来限制通道的行为,一般会在函数的形参和返回值中出现,
例如用于关闭通道的内置函数close的函数签名就用到了单向通道。
func close(c chan<- Type)
---------------------------------------------------------------------------------------------------------
又或者说常用到的time包下的After函数
func After(d Duration) <-chan Time
---------------------------------------------------------------------------------------------------------
close函数的形参是一个只写通道,After函数的返回值是一个只读通道,所以单向通道的语法如下:
箭头符号<-在前,就是只读通道,如<-chan int
箭头符号<-在后,就是只写通道,如chan<- string
---------------------------------------------------------------------------------------------------------
当尝试对只读的管道写入数据时,将会无法通过编译
func main() {
timeCh := time.After(time.Second)
timeCh <- time.Now()
}
报错如下,意思非常明确
invalid operation: cannot send to receive-only channel timeCh (variable of type <-chan time.Time)
对只写的管道读取数据也是同理。
---------------------------------------------------------------------------------------------------------
双向管道可以转换为单向管道,反过来则不可以。
通常情况下,将双向管道传给某个协程或函数并且不希望它读取/发送数据,就可以用到单向管道来限制另一方的行为。
func main() {
ch := make(chan int, 1)
go write(ch)
fmt.Println(<-ch)
}
func write(ch chan<- int) {
// 只能对管道发送数据
ch <- 1
}
只读管道也是一样的道理
---------------------------------------------------------------------------------------------------------
提示
chan是引用类型,即便Go的函数参数是值传递,但其引用依旧是同一个,这一点会在后续的管道原理中说明。
07.for range
通过for range语句,可以遍历读取缓冲管道中的数据,如下例
func main() {
ch := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
}()
for n := range ch {
fmt.Println(n)
}
}
---------------------------------------------------------------------------------------------------------
通常来说,for range遍历其他可迭代数据结构时,会有两个返回值,第一个是索引,第二个元素值,但是对于管道而言,
有且仅有一个返回值,for range会不断读取管道中的元素,当管道缓冲区为空或无缓冲时,就会阻塞等待,
直到有其他协程向管道中写入数据才会继续读取数据。所以输出如下:
0
1
2
3
4
5
6
7
8
9
fatal error: all goroutines are asleep - deadlock!
---------------------------------------------------------------------------------------------------------
可以看到上面的代码发生了死锁,因为子协程已经执行完毕了,而主协程还在阻塞等待其他协程来向管道中写入数据,
所以应该管道在写入完毕后将其关闭。修改为如下代码
func main() {
ch := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
// 关闭管道
close(ch)
}()
for n := range ch {
fmt.Println(n)
}
}
---------------------------------------------------------------------------------------------------------
写完后关闭管道,上述代码便不再会发生死锁。前面提到过读取管道是有两个返回值的,for range遍历管道时,
当无法成功读取数据时,便会退出循环。第二个返回值指的是能否成功读取数据,而不是管道是否已经关闭,
即便管道已经关闭,对于有缓冲管道而言,依旧可以读取数据,并且第二个返回值仍然为true。看下面的一个例子
func main() {
ch := make(chan int, 10)
for i := 0; i < 5; i++ {
ch <- i
}
// 关闭管道
close(ch)
// 再读取数据
for i := 0; i < 6; i++ {
n, ok := <-ch
fmt.Println(n, ok)
}
}
输出结果
0 true
1 true
2 true
3 true
4 true
0 false
---------------------------------------------------------------------------------------------------------
由于管道已经关闭了,即便缓冲区为空,再读取数据也不会导致当前协程阻塞,可以看到在第六次遍历的时候读取的是零值,并且ok为false。
提示:关于管道关闭的时机,应该尽量在向管道发送数据的那一方关闭管道,而不要在接收方关闭管道,因为大多数情况下接收方只知道接收数据,并不知道该在什么时候关闭管道。
08.select
a.介绍
select在Linux系统中,是一种IO多路复用的解决方案,类似的,在Go中,select是一种管道多路复用的控制结构。
什么是多路复用,简单的用一句话概括:在某一时刻,同时监测多个元素是否可用,被监测的可以是网络请求,文件IO等。
在Go中的select监测的元素就是管道,且只能是管道。select的语法与switch语句类似,下面看看一个select语句长什么样
func main() {
// 创建三个管道
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
select {
case n, ok := <-chA:
fmt.Println(n, ok)
case n, ok := <-chB:
fmt.Println(n, ok)
case n, ok := <-chC:
fmt.Println(n, ok)
default:
fmt.Println("所有管道都不可用")
}
}
-----------------------------------------------------------------------------------------------------
与switch类似,select由多个case和一个default组成,default分支可以省略。
每一个case只能操作一个管道,且只能进行一种操作,要么读要么写,
当有多个case可用时,select会伪随机的选择一个case来执行。如果所有case都不可用,
就会执行default分支,倘若没有default分支,将会阻塞等待,直到至少有一个case可用。
由于上例中没有对管道写入数据,自然所有的case都不可用,所以最终输出为default分支的执行结果。稍微修改下后如下:
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
// 开启一个新的协程
go func() {
// 向A管道写入数据
chA <- 1
}()
select {
case n, ok := <-chA:
fmt.Println(n, ok)
case n, ok := <-chB:
fmt.Println(n, ok)
case n, ok := <-chC:
fmt.Println(n, ok)
}
}
-----------------------------------------------------------------------------------------------------
上例开启了一个新的协程来向管道A写入数据,select由于没有默认分支,所以会一直阻塞等待直到有case可用。
当管道A可用时,执行完对应分支后主协程就直接退出了。要想一直监测管道,可以配合for循环使用,如下。
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
go Send(chA)
go Send(chB)
go Send(chC)
// for循环
for {
select {
case n, ok := <-chA:
fmt.Println("A", n, ok)
case n, ok := <-chB:
fmt.Println("B", n, ok)
case n, ok := <-chC:
fmt.Println("C", n, ok)
}
}
}
func Send(ch chan<- int) {
for i := 0; i < 3; i++ {
time.Sleep(time.Millisecond)
ch <- i
}
}
-----------------------------------------------------------------------------------------------------
这样确实三个管道都能用上了,但是死循环+select会导致主协程永久阻塞,所以可以将其单独放到新协程中,并且加上一些其他的逻辑。
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
l := make(chan struct{})
go Send(chA)
go Send(chB)
go Send(chC)
go func() {
Loop:
for {
select {
case n, ok := <-chA:
fmt.Println("A", n, ok)
case n, ok := <-chB:
fmt.Println("B", n, ok)
case n, ok := <-chC:
fmt.Println("C", n, ok)
case <-time.After(time.Second): // 设置1秒的超时时间
break Loop // 退出循环
}
}
l <- struct{}{} // 告诉主协程可以退出了
}()
<-l
}
func Send(ch chan<- int) {
for i := 0; i < 3; i++ {
time.Sleep(time.Millisecond)
ch <- i
}
}
-----------------------------------------------------------------------------------------------------
上例中通过for循环配合select来一直监测三个管道是否可以用,并且第四个case是一个超时管道,超时过后便会退出循环,结束子协程。最终输出如下
C 0 true
A 0 true
B 0 true
A 1 true
B 1 true
C 1 true
B 2 true
C 2 true
A 2 true
b.超时
上一个例子用到了time.After函数,其返回值是一个只读的管道,该函数配合select使用可以非常简单的实现超时机制,例子如下
func main() {
chA := make(chan int)
defer close(chA)
go func() {
time.Sleep(time.Second * 2)
chA <- 1
}()
select {
case n := <-chA:
fmt.Println(n)
case <-time.After(time.Second):
fmt.Println("超时")
}
}
c.永久阻塞
当select语句中什么都没有时,就会永久阻塞,例如
func main() {
fmt.Println("start")
select {}
fmt.Println("end")
}
end永远也不会输出,主协程会一直阻塞,这种情况一般是有特殊用途。
d.提示
在select的case中对值为nil的管道进行操作的话,并不会导致阻塞,该case则会被忽略,永远也不会被执行。
例如下方代码无论执行多少次都只会输出timeout。
func main() {
var nilCh chan int
select {
case <-nilCh:
fmt.Println("read")
case nilCh <- 1:
fmt.Println("write")
case <-time.After(time.Second):
fmt.Println("timeout")
}
}
1.7 WaitGroup
01.介绍
sync.WaitGroup是sync包下提供的一个结构体,WaitGroup即等待执行,使用它可以很轻易的实现等待一组协程的效果。该结构体只对外暴露三个方法。
---------------------------------------------------------------------------------------------------------
Add方法用于指明要等待的协程的数量
func (wg *WaitGroup) Add(delta int)
---------------------------------------------------------------------------------------------------------
Done方法表示当前协程已经执行完毕
func (wg *WaitGroup) Done()
---------------------------------------------------------------------------------------------------------
Wait方法等待子协程结束,否则就阻塞
func (wg *WaitGroup) Wait()
02.使用
WaitGroup使用起来十分简单,属于开箱即用。其内部的实现是计数器+信号量,程序开始时调用Add初始化计数,
每当一个协程执行完毕时调用Done,计数就-1,直到减为0,而在此期间,主协程调用Wait 会一直阻塞直到全部计数减为0,
然后才会被唤醒。看一个简单的使用例子
func main() {
var wait sync.WaitGroup
// 指定子协程的数量
wait.Add(1)
go func() {
fmt.Println(1)
// 执行完毕
wait.Done()
}()
// 等待子协程
wait.Wait()
fmt.Println(2)
}
---------------------------------------------------------------------------------------------------------
这段代码永远都是先输出1再输出2,主协程会等待子协程执行完毕后再退出。
1
2
---------------------------------------------------------------------------------------------------------
针对协程介绍中最开始的例子,可以做出如下修改
func main() {
var mainWait sync.WaitGroup
var wait sync.WaitGroup
// 计数10
mainWait.Add(10)
fmt.Println("start")
for i := 0; i < 10; i++ {
// 循环内计数1
wait.Add(1)
go func() {
fmt.Println(i)
// 两个计数-1
wait.Done()
mainWait.Done()
}()
// 等待当前循环的协程执行完毕
wait.Wait()
}
// 等待所有的协程执行完毕
mainWait.Wait()
fmt.Println("end")
}
---------------------------------------------------------------------------------------------------------
这里使用了sync.WaitGroup替代了原先的time.Sleep,协程并发执行的的顺序更加可控,不管执行多少次,输出都如下
start
0
1
2
3
4
5
6
7
8
9
end
---------------------------------------------------------------------------------------------------------
WaitGroup通常适用于可动态调整协程数量的时候,例如事先知晓协程的数量,又或者在运行过程中需要动态调整。
WaitGroup的值不应该被复制,复制后的值也不应该继续使用,尤其是将其作为函数参数传递时,因该传递指针而不是值。
倘若使用复制的值,计数完全无法作用到真正的WaitGroup上,这可能会导致主协程一直阻塞等待,程序将无法正常运行。
例如下方的代码
func main() {
var mainWait sync.WaitGroup
mainWait.Add(1)
hello(mainWait)
mainWait.Wait()
fmt.Println("end")
}
func hello(wait sync.WaitGroup) {
fmt.Println("hello")
wait.Done()
}
---------------------------------------------------------------------------------------------------------
错误提示所有的协程都已经退出,但主协程依旧在等待,这就形成了死锁,
因为hello函数内部对一个形参WaitGroup调用Done并不会作用到原来的mainWait上,所以应该使用指针来进行传递。
hello
fatal error: all goroutines are asleep - deadlock!
---------------------------------------------------------------------------------------------------------
提示
当计数变为负数,或者计数数量大于子协程数量时,将会引发panic。
1.8 Context
00.介绍
Context译为上下文,是Go提供的一种并发控制的解决方案,相比于管道和WaitGroup,它可以更好的控制子孙协程以及层级更深的协程。
Context本身是一个接口,只要实现了该接口都可以称之为上下文例如著名Web框架Gin中的gin.Context。
context标准库也提供了几个实现,分别是:
emptyCtx
cancelCtx
timerCtx
valueCtx
01.Context
先来看看Context接口的定义,再去了解它的具体实现。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
---------------------------------------------------------------------------------------------------------
Deadline
该方法具有两个返回值,deadline是截止时间,即上下文应该取消的时间。第二个值是是否设置deadline,如果没有设置则一直为false。
Deadline() (deadline time.Time, ok bool)
---------------------------------------------------------------------------------------------------------
Done
其返回值是一个空结构体类型的只读管道,该管道仅仅起到通知作用,不传递任何数据。当上下文所做的工作应该取消时,该通道就会被关闭,对于一些不支持取消的上下文,可能会返回nil。
Done() <-chan struct{}
---------------------------------------------------------------------------------------------------------
Err
该方法返回一个error,用于表示上下关闭的原因。当Done管道没有关闭时,返回nil,如果关闭过后,会返回一个err来解释为什么会关闭。
Err() error
---------------------------------------------------------------------------------------------------------
Value
该方法返回对应的键值,如果key不存在,或者不支持该方法,就会返回nil。
Value(key any) any
02.emptyCtx
顾名思义,emptyCtx就是空的上下文,context包下所有的实现都是不对外暴露的,但是提供了对应的函数来创建上下文。
emptyCtx就可以通过context.Background和context.TODO来进行创建。两个函数如下
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
---------------------------------------------------------------------------------------------------------
可以看到仅仅只是返回了emptyCtx指针。emptyCtx的底层类型实际上是一个int,之所以不使用空结构体,
是因为emptyCtx的实例必须要有不同的内存地址,它没法被取消,没有deadline,也不能取值,实现的方法都是返回零值。
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key any) any {
return nil
}
---------------------------------------------------------------------------------------------------------
emptyCtx通常是用来当作最顶层的上下文,在创建其他三种上下文时作为父上下文传入。
03.valueCtx
valueCtx实现比较简单,其内部只包含一对键值对,和一个内嵌的Context类型的字段。
type valueCtx struct {
Context
key, val any
}
---------------------------------------------------------------------------------------------------------
其本身只实现了Value方法,逻辑也很简单,当前上下文找不到就去父上下文找。
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
---------------------------------------------------------------------------------------------------------
下面看一个valueCtx的简单使用案例
var waitGroup sync.WaitGroup
func main() {
waitGroup.Add(1)
// 传入上下文
go Do(context.WithValue(context.Background(), 1, 2))
waitGroup.Wait()
}
func Do(ctx context.Context) {
// 新建定时器
ticker := time.NewTimer(time.Second)
defer waitGroup.Done()
for {
select {
case <-ctx.Done(): // 永远也不会执行
case <-ticker.C:
fmt.Println("timeout")
return
default:
fmt.Println(ctx.Value(1))
}
time.Sleep(time.Millisecond * 100)
}
}
---------------------------------------------------------------------------------------------------------
valueCtx多用于在多级协程中传递一些数据,无法被取消,因此ctx.Done永远会返回nil,select会忽略掉nil管道。最后输出如下
2
2
2
2
2
2
2
2
2
2
timeout
04.cancelCtx
cancelCtx以及timerCtx都实现了canceler接口,接口类型如下
type canceler interface {
// removeFromParent 表示是否从父上下文中删除自身
// err 表示取消的原因
cancel(removeFromParent bool, err error)
// Done 返回一个管道,用于通知取消的原因
Done() <-chan struct{}
}
---------------------------------------------------------------------------------------------------------
cancel方法不对外暴露,在创建上下文时通过闭包将其包装为返回值以供外界调用,就如context.WithCancel源代码中所示
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
// 尝试将自身添加进父级的children中
propagateCancel(parent, &c)
// 返回context和一个函数
return &c, func() { c.cancel(true, Canceled) }
}
---------------------------------------------------------------------------------------------------------
cancelCtx译为可取消的上下文,创建时,如果父级实现了canceler,就会将自身添加进父级的children中,否则就一直向上查找。
如果所有的父级都没有实现canceler,就会启动一个协程等待父级取消,然后当父级结束时取消当前上下文。当调用cancelFunc时,
Done通道将会关闭,该上下文的任何子级也会随之取消,最后会将自身从父级中删除。下面是一个简单的示例:
var waitGroup sync.WaitGroup
func main() {
bkg := context.Background()
// 返回了一个cancelCtx和cancel函数
cancelCtx, cancel := context.WithCancel(bkg)
waitGroup.Add(1)
go func(ctx context.Context) {
defer waitGroup.Done()
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("等待取消中...")
}
time.Sleep(time.Millisecond * 200)
}
}(cancelCtx)
time.Sleep(time.Second)
cancel()
waitGroup.Wait()
}
输出如下
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
context canceled
---------------------------------------------------------------------------------------------------------
再来一个层级嵌套深一点的示例
var waitGroup sync.WaitGroup
func main() {
waitGroup.Add(3)
ctx, cancelFunc := context.WithCancel(context.Background())
go HttpHandler(ctx)
time.Sleep(time.Second)
cancelFunc()
waitGroup.Wait()
}
func HttpHandler(ctx context.Context) {
cancelCtxAuth, cancelAuth := context.WithCancel(ctx)
cancelCtxMail, cancelMail := context.WithCancel(ctx)
defer cancelAuth()
defer cancelMail()
defer waitGroup.Done()
go AuthService(cancelCtxAuth)
go MailService(cancelCtxMail)
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("正在处理http请求...")
}
time.Sleep(time.Millisecond * 200)
}
}
func AuthService(ctx context.Context) {
defer waitGroup.Done()
for {
select {
case <-ctx.Done():
fmt.Println("auth 父级取消", ctx.Err())
return
default:
fmt.Println("auth...")
}
time.Sleep(time.Millisecond * 200)
}
}
func MailService(ctx context.Context) {
defer waitGroup.Done()
for {
select {
case <-ctx.Done():
fmt.Println("mail 父级取消", ctx.Err())
return
default:
fmt.Println("mail...")
}
time.Sleep(time.Millisecond * 200)
}
}
---------------------------------------------------------------------------------------------------------
例子中创建了3个cancelCtx,尽管父级cancelCtx在取消的同时会取消它的子上下文,但是保险起见,
如果创建了一个cancelCtx,在相应的流程结束后就应该调用cancel函数。输出如下
正在处理http请求...
auth...
mail...
mail...
auth...
正在处理http请求...
auth...
mail...
正在处理http请求...
正在处理http请求...
auth...
mail...
auth...
正在处理http请求...
mail...
context canceled
auth 父级取消 context canceled
mail 父级取消 context canceled
05.timerCtx
timerCtx在cancelCtx 的基础之上增加了超时机制,context包下提供了两种创建的函数,分别是WithDeadline和WithTimeout,两者功能类似,前者是指定一个具体的超时时间,比如指定一个具体时间2023/3/20 16:32:00,后者是指定一个超时的时间间隔,比如5分钟后。两个函数的签名如下
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
---------------------------------------------------------------------------------------------------------
timerCtx会在时间到期后自动取消当前上下文,取消的流程除了要额外的关闭timer之外,基本与cancelCtx一致。下面是一个简单的timerCtx的使用示例
var wait sync.WaitGroup
func main() {
deadline, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
defer cancel()
wait.Add(1)
go func(ctx context.Context) {
defer wait.Done()
for {
select {
case <-ctx.Done():
fmt.Println("上下文取消", ctx.Err())
return
default:
fmt.Println("等待取消中...")
}
time.Sleep(time.Millisecond * 200)
}
}(deadline)
wait.Wait()
}
---------------------------------------------------------------------------------------------------------
尽管上下文到期会自动取消,但是为了保险起见,在相关流程结束后,最好手动取消上下文。输出如下
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
上下文取消 context deadline exceeded
---------------------------------------------------------------------------------------------------------
WithTimeout其实与WithDealine非常相似,它的实现也只是稍微封装了一下并调用WithDeadline,和上面例子中的WithDeadline用法一样,如下
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
---------------------------------------------------------------------------------------------------------
提示
就跟内存分配后不回收会造成内存泄漏一样,上下文也是一种资源,如果创建了但从来不取消,一样会造成上下文泄露,所以最好避免此种情况的发生。
1.9 锁
00.介绍
先来看看的一个例子
var wait sync.WaitGroup
var count = 0
func main() {
wait.Add(10)
for i := 0; i < 10; i++ {
go func(data *int) {
// 模拟访问耗时
time.Sleep(time.Millisecond * time.Duration(rand.Intn(5000)))
// 访问数据
temp := *data
// 模拟计算耗时
time.Sleep(time.Millisecond * time.Duration(rand.Intn(5000)))
ans := 1
// 修改数据
*data = temp + ans
fmt.Println(*data)
wait.Done()
}(&count)
}
wait.Wait()
fmt.Println("最终结果", count)
}
---------------------------------------------------------------------------------------------------------
对于上面的例子,开启了十个协程来对count进行+1操作,并且使用了time.Sleep来模拟不同的耗时,根据直觉来讲,10个协程执行10个+1操作,最终结果一定是10,正确结果也确实是10,但事实并非如此,上面的例子执行结果如下:
1
2
3
3
2
2
3
3
3
4
最终结果 4
---------------------------------------------------------------------------------------------------------
可以看到最终结果为4,而这只是众多可能结果中的一种。由于每个协程访问和计算所需的时间不同,A协程访问数据耗费500毫秒,此时访问到的count值为1,随后又花费了400毫秒计算,但在这400毫秒内,B协程已经完成了访问和计算并成功更新了count的值,A协程在计算完毕后,A协程最初访问到的值已经过时了,但A协程并不知道这件事,依旧在原先访问到的值基础上加一,并赋值给count,这样一来,B协程的执行结果被覆盖了。多个协程读取和访问一个共享数据时,尤其会发生这样的问题,为此就需要用到锁。
Go中sync包下的Mutex与RWMutex提供了互斥锁与读写锁两种实现,且提供了非常简单易用的API,加锁只需要Lock(),解锁也只需要Unlock()。需要注意的是,Go所提供的锁都是非递归锁,也就是不可重入锁,所以重复加锁或重复解锁都会导致fatal。锁的意义在于保护不变量,加锁是希望数据不会被其他协程修改,如下
func DoSomething() {
Lock()
// 在这个过程中,数据不会被其他协程修改
Unlock()
}
---------------------------------------------------------------------------------------------------------
倘若是递归锁的话,就可能会发生如下情况
func DoSomething() {
Lock()
DoOther()
Unlock()
}
func DoOther() {
Lock()
// do other
Unlock()
}
---------------------------------------------------------------------------------------------------------
DoSomthing函数显然不知道DoOther函数可能会对数据做点什么,从而修改了数据,比如再开几个子协程破坏了不变量。这在Go中是行不通的,一旦加锁以后就必须保证不变量的不变性,此时重复加锁解锁都会导致死锁。所以在编写代码时应该避免上述情况,必要时在加锁的同时立即使用defer语句解锁。
01.互斥锁
sync.Mutex是Go提供的互斥锁实现,其实现了sync.Locker接口
type Locker interface {
// 加锁
Lock()
// 解锁
Unlock()
}
---------------------------------------------------------------------------------------------------------
使用互斥锁可以非常完美的解决上述问题,例子如下
var wait sync.WaitGroup
var count = 0
var lock sync.Mutex
func main() {
wait.Add(10)
for i := 0; i < 10; i++ {
go func(data *int) {
// 加锁
lock.Lock()
// 模拟访问耗时
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
// 访问数据
temp := *data
// 模拟计算耗时
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
ans := 1
// 修改数据
*data = temp + ans
// 解锁
lock.Unlock()
fmt.Println(*data)
wait.Done()
}(&count)
}
wait.Wait()
fmt.Println("最终结果", count)
}
---------------------------------------------------------------------------------------------------------
每一个协程在访问数据前,都先上锁,更新完成后再解锁,其他协程想要访问就必须要先获得锁,否则就阻塞等待。如此一来,就不存在上述问题了,所以输出如下
1
2
3
4
5
6
7
8
9
10
最终结果 10
02.读写锁
互斥锁适合读操作与写操作频率都差不多的情况,对于一些读多写少的数据,如果使用互斥锁,会造成大量的不必要的协程竞争锁,这会消耗很多的系统资源,这时候就需要用到读写锁,即读写互斥锁,对于一个协程而言:
如果获得了读锁,其他协程进行写操作时会阻塞,其他协程进行读操作时不会阻塞
如果获得了写锁,其他协程进行写操作时会阻塞,其他协程进行读操作时会阻塞
---------------------------------------------------------------------------------------------------------
Go中读写互斥锁的实现是sync.RWMutex,它也同样实现了Locker接口,但它提供了更多可用的方法,如下:
// 加读锁
func (rw *RWMutex) RLock()
// 尝试加读锁
func (rw *RWMutex) TryRLock() bool
// 解读锁
func (rw *RWMutex) RUnlock()
// 加写锁
func (rw *RWMutex) Lock()
// 尝试加写锁
func (rw *RWMutex) TryLock() bool
// 解写锁
func (rw *RWMutex) Unlock()
---------------------------------------------------------------------------------------------------------
其中TryRlock与TryLock两个尝试加锁的操作是非阻塞式的,成功加锁会返回true,无法获得锁时并不会阻塞而是返回false。
读写互斥锁内部实现依旧是互斥锁,并不是说分读锁和写锁就有两个锁,从始至终都只有一个锁。
下面来看一个读写互斥锁的使用案例
var wait sync.WaitGroup
var count = 0
var rw sync.RWMutex
func main() {
wait.Add(12)
// 读多写少
go func() {
for i := 0; i < 3; i++ {
go Write(&count)
}
wait.Done()
}()
go func() {
for i := 0; i < 7; i++ {
go Read(&count)
}
wait.Done()
}()
// 等待子协程结束
wait.Wait()
fmt.Println("最终结果", count)
}
func Read(i *int) {
time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))
rw.RLock()
fmt.Println("拿到读锁")
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
fmt.Println("释放读锁", *i)
rw.RUnlock()
wait.Done()
}
func Write(i *int) {
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
rw.Lock()
fmt.Println("拿到写锁")
temp := *i
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
*i = temp + 1
fmt.Println("释放写锁", *i)
rw.Unlock()
wait.Done()
}
---------------------------------------------------------------------------------------------------------
该例开启了3个写协程,7个读协程,在读数据的时候都会先获得读锁,读协程可以正常获得读锁,但是会阻塞写协程,
获得写锁的时候,则会同时阻塞读协程和写协程,直到释放写锁,如此一来实现了读协程与写协程互斥,保证了数据的正确性。
例子输出如下:
拿到读锁
拿到读锁
拿到读锁
拿到读锁
释放读锁 0
释放读锁 0
释放读锁 0
释放读锁 0
拿到写锁
释放写锁 1
拿到读锁
拿到读锁
拿到读锁
释放读锁 1
释放读锁 1
释放读锁 1
拿到写锁
释放写锁 2
拿到写锁
释放写锁 3
最终结果 3
---------------------------------------------------------------------------------------------------------
提示
对于锁而言,不应该将其作为值传递和存储,应该使用指针。
03.条件变量
条件变量,与互斥锁一同出现和使用,所以有些人可能会误称为条件锁,但它并不是锁,是一种通讯机制。Go中的sync.Cond对此提供了实现,而创建条件变量的函数签名如下:
func NewCond(l Locker) *Cond
---------------------------------------------------------------------------------------------------------
可以看到创建一个条件变量前提就是需要创建一个锁,sync.Cond提供了如下的方法以供使用
// 阻塞等待条件生效,直到被唤醒
func (c *Cond) Wait()
// 唤醒一个因条件阻塞的协程
func (c *Cond) Signal()
// 唤醒所有因条件阻塞的协程
func (c *Cond) Broadcast()
---------------------------------------------------------------------------------------------------------
条件变量使用起来非常简单,将上面的读写互斥锁的例子稍微修改下即可
var wait sync.WaitGroup
var count = 0
var rw sync.RWMutex
// 条件变量
var cond = sync.NewCond(rw.RLocker())
func main() {
wait.Add(12)
// 读多写少
go func() {
for i := 0; i < 3; i++ {
go Write(&count)
}
wait.Done()
}()
go func() {
for i := 0; i < 7; i++ {
go Read(&count)
}
wait.Done()
}()
// 等待子协程结束
wait.Wait()
fmt.Println("最终结果", count)
}
func Read(i *int) {
time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))
rw.RLock()
fmt.Println("拿到读锁")
// 条件不满足就一直阻塞
for *i < 3 {
cond.Wait()
}
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
fmt.Println("释放读锁", *i)
rw.RUnlock()
wait.Done()
}
func Write(i *int) {
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
rw.Lock()
fmt.Println("拿到写锁")
temp := *i
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
*i = temp + 1
fmt.Println("释放写锁", *i)
rw.Unlock()
// 唤醒所有因条件变量阻塞的协程
cond.Broadcast()
wait.Done()
}
---------------------------------------------------------------------------------------------------------
在创建条件变量时,因为在这里条件变量作用的是读协程,所以将读锁作为互斥锁传入,
如果直接传入读写互斥锁会导致写协程重复解锁的问题。这里传入的是sync.rlocker,通过RWMutex.RLocker方法获得。
func (rw *RWMutex) RLocker() Locker {
return (*rlocker)(rw)
}
type rlocker RWMutex
func (r *rlocker) Lock() { (*RWMutex)(r).RLock() }
func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() }
---------------------------------------------------------------------------------------------------------
可以看到rlocker也只是把读写互斥锁的读锁操作封装了一下,实际上是同一个引用,依旧是同一个锁。读协程读取数据时,
如果小于3就会一直阻塞等待,直到数据大于3,而写协程在更新数据后都会尝试唤醒所有因条件变量而阻塞的协程,
所以最后的输出如下
拿到读锁
拿到读锁
拿到读锁
拿到读锁
拿到写锁
释放写锁 1
拿到读锁
拿到写锁
释放写锁 2
拿到读锁
拿到读锁
拿到写锁
释放写锁 3 // 第三个写协程执行完毕
释放读锁 3
释放读锁 3
释放读锁 3
释放读锁 3
释放读锁 3
释放读锁 3
释放读锁 3
最终结果 3
从结果中可以看到,当第三个写协程更新完数据后,七个因条件变量而阻塞的读协程都恢复了运行。
---------------------------------------------------------------------------------------------------------
提示
对于条件变量,应该使用for而不是if,应该使用循环来判断条件是否满足,因为协程被唤醒时并不能保证当前条件就已经满足了。
for !condition {
cond.Wait()
}
1.10 sync
00.汇总
Go中很大一部分的并发相关的工具都是sync标准库提供的,上述已经介绍过了sync.WaitGroup,sync.Locker等,
除此之外,sync包下还有一些其他的工具可以使用。
01.Once
当在使用一些数据结构时,如果这些数据结构太过庞大,可以考虑采用懒加载的方式,即真正要用到它的时候才会初始化该数据结构。如下面的例子
type MySlice []int
func (m *MySlice) Get(i int) (int, bool) {
if *m == nil {
return 0, false
} else {
return (*m)[i], true
}
}
func (m *MySlice) Add(i int) {
// 当真正用到切片的时候,才会考虑去初始化
if *m == nil {
*m = make([]int, 0, 10)
}
*m = append(*m, i)
}
---------------------------------------------------------------------------------------------------------
那么问题就来了,如果只有一个协程使用肯定是没有任何问题的,但是如果有多个协程访问的话就可能会出现问题了。
比如协程A和B同时调用了Add方法,A执行的稍微快一些,已经初始化完毕了,并且将数据成功添加,随后协程B又初始化了一遍,
这样一来将协程A添加的数据直接覆盖掉了,这就是问题所在。
---------------------------------------------------------------------------------------------------------
而这就是sync.Once要解决的问题,顾名思义,Once译为一次,sync.Once保证了在并发条件下指定操作只会执行一次。
它的使用非常简单,只对外暴露了一个Do方法,签名如下:
func (o *Once) Do(f func())
---------------------------------------------------------------------------------------------------------
在使用时,只需要将初始化操作传入Do方法即可,如下
var wait sync.WaitGroup
func main() {
var slice MySlice
wait.Add(4)
for i := 0; i < 4; i++ {
go func() {
slice.Add(1)
wait.Done()
}()
}
wait.Wait()
fmt.Println(slice.Len())
}
type MySlice struct {
s []int
o sync.Once
}
func (m *MySlice) Get(i int) (int, bool) {
if m.s == nil {
return 0, false
} else {
return m.s[i], true
}
}
func (m *MySlice) Add(i int) {
// 当真正用到切片的时候,才会考虑去初始化
m.o.Do(func() {
fmt.Println("初始化")
if m.s == nil {
m.s = make([]int, 0, 10)
}
})
m.s = append(m.s, i)
}
func (m *MySlice) Len() int {
return len(m.s)
}
输出如下
初始化
4
---------------------------------------------------------------------------------------------------------
从输出结果中可以看到,所有的数据等正常添加进切片,初始化操作只执行了一次。其实sync.Once的实现相当简单,
去除注释真正的代码逻辑只有16行,其原理就是锁+原子操作。源代码如下:
type Once struct {
// 用于判断操作是否已经执行
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
// 原子加载数据
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
// 加锁
o.m.Lock()
// 解锁
defer o.m.Unlock()
// 判断是否执行
if o.done == 0 {
// 执行完毕后修改done
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
02.Pool
sync.Pool的设计目的是用于存储临时对象以便后续的复用,是一个临时的并发安全对象池,
将暂时用不到的对象放入池中,在后续使用中就不需要再额外的创建对象可以直接复用,减少内存的分配与释放频率,
最重要的一点就是降低GC压力。sync.Pool总共只有两个方法,如下:
---------------------------------------------------------------------------------------------------------
// 申请一个对象
func (p *Pool) Get() any
// 放入一个对象
func (p *Pool) Put(x any)
---------------------------------------------------------------------------------------------------------
并且sync.Pool有一个对外暴露的New字段,用于对象池在申请不到对象时初始化一个对象
New func() any
---------------------------------------------------------------------------------------------------------
下面以一个例子演示
var wait sync.WaitGroup
// 临时对象池
var pool sync.Pool
// 用于计数过程中总共创建了多少个对象
var numOfObject atomic.Int64
// BigMemData 假设这是一个占用内存很大的结构体
type BigMemData struct {
M string
}
func main() {
pool.New = func() any {
numOfObject.Add(1)
return BigMemData{"大内存"}
}
wait.Add(1000)
// 这里开启1000个协程
for i := 0; i < 1000; i++ {
go func() {
// 申请对象
val := pool.Get()
// 使用对象
_ = val.(BigMemData)
// 用完之后再释放对象
pool.Put(val)
wait.Done()
}()
}
wait.Wait()
fmt.Println(numOfObject.Load())
}
---------------------------------------------------------------------------------------------------------
例子中开启了1000个协程不断的在池中申请和释放对象,如果不采用对象池,那么1000个协程都需要各自实例化对象,
并且这1000个实例化后的对象在使用完毕后都需要由GC来释放内存,如果有几十万个协程或者说创建该对象的成本十分的高昂,
这种情况下就会占用很大的内存并且给GC带来非常大的压力,采用对象池后,可以复用对象减少实例化的频率,
比如上述的例子输出可能如下:
5
---------------------------------------------------------------------------------------------------------
即便开启了1000个协程,整个过程中也只创建了5个对象,如果不采用对象池的话1000个协程将会创建1000个对象,
这种优化带来的提升是显而易见的,尤其是在并发量特别大和实例化对象成本特别高的时候更能体现出优势。
在使用sync.Pool时需要注意几个点:
临时对象:sync.Pool只适合存放临时对象,池中的对象可能会在没有任何通知的情况下被GC移除,所以并不建议将网络链接,数据库连接这类存入sync.Pool中。
不可预知:sync.Pool在申请对象时,无法预知这个对象是新创建的还是复用的,也无法知晓池中有几个对象
并发安全:官方保证sync.Pool一定是并发安全,但并不保证用于创建对象的New函数就一定是并发安全的,New函数是由使用者传入的,所以New函数的并发安全性要由使用者自己来维护,这也是为什么上例中对象计数要用到原子值的原因。
提示,最后需要注意的是,当使用完对象后,一定要释放回池中,如果用了不释放那么对象池的使用将毫无意义。
---------------------------------------------------------------------------------------------------------
标准库fmt包下就有一个对象池的使用案例,在fmt.Fprintf函数中
func Fprintf(w io.Writer, format string, a ...any) (n int, err error) {
// 申请一个打印缓冲区
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
// 使用完毕后释放
p.free()
return
}
---------------------------------------------------------------------------------------------------------
其中newPointer函数和free方法的实现如下
func newPrinter() *pp {
// 向对象池申请的一个对象
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}
func (p *pp) free() {
// 为了让对象池中的缓冲区大小大致相同以便更好的弹性控制缓冲区大小
// 过大的缓冲区就不用放回对象池
if cap(p.buf) > 64<<10 {
return
}
// 字段重置后释放对象到池中
p.buf = p.buf[:0]
p.arg = nil
p.value = reflect.Value{}
p.wrappedErr = nil
ppFree.Put(p)
}
03.Map
sync.Map是官方提供的一种并发安全Map的实现,开箱即用,使用起来十分的简单,下面是该结构体对外暴露的方法:
// 根据一个key读取值,返回值会返回对应的值和该值是否存在
func (m *Map) Load(key any) (value any, ok bool)
// 存储一个键值对
func (m *Map) Store(key, value any)
// 删除一个键值对
func (m *Map) Delete(key any)
// 如果该key已存在,就返回原有的值,否则将新的值存入并返回,当成功读取到值时,loaded为true,否则为false
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
// 删除一个键值对,并返回其原有的值,loaded的值取决于key是否存在
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
// 遍历Map,当f()返回false时,就会停止遍历
func (m *Map) Range(f func(key, value any) bool)
---------------------------------------------------------------------------------------------------------
下面用一个简单的示例来演示下sync.Map的基本使用
func main() {
var syncMap sync.Map
// 存入数据
syncMap.Store("a", 1)
syncMap.Store("a", "a")
// 读取数据
fmt.Println(syncMap.Load("a"))
// 读取并删除
fmt.Println(syncMap.LoadAndDelete("a"))
// 读取或存入
fmt.Println(syncMap.LoadOrStore("a", "hello world"))
syncMap.Store("b", "goodbye world")
// 遍历map
syncMap.Range(func(key, value any) bool {
fmt.Println(key, value)
return true
})
}
输出
a true
a true
hello world false
a hello world
b goodbye world
---------------------------------------------------------------------------------------------------------
接下来看一个并发使用map的例子:
func main() {
myMap := make(map[int]int, 10)
var wait sync.WaitGroup
wait.Add(10)
for i := 0; i < 10; i++ {
go func(n int) {
for i := 0; i < 100; i++ {
myMap[n] = n
}
wait.Done()
}(i)
}
wait.Wait()
}
---------------------------------------------------------------------------------------------------------
上例中使用的普通map,开了10个协程不断的存入数据,显然这很可能会触发fatal,结果大概率会如下
fatal error: concurrent map writes
使用sync.Map就可以避免这个问题
func main() {
var syncMap sync.Map
var wait sync.WaitGroup
wait.Add(10)
for i := 0; i < 10; i++ {
go func(n int) {
for i := 0; i < 100; i++ {
syncMap.Store(n, n)
}
wait.Done()
}(i)
}
wait.Wait()
syncMap.Range(func(key, value any) bool {
fmt.Println(key, value)
return true
})
}
输出如下
8 8
3 3
1 1
9 9
6 6
5 5
7 7
0 0
2 2
4 4
---------------------------------------------------------------------------------------------------------
为了并发安全肯定需要做出一定的牺牲,sync.Map的性能要比map低10-100倍左右。
1.11 原子
00.介绍
在计算机学科中,原子或原语操作,通常用于表述一些不可再细化分割的操作,由于这些操作无法再细化为更小的步骤,
在执行完毕前,不会被其他的任何协程打断,所以执行结果要么成功要么失败,没有第三种情况可言,
如果出现了其他情况,那么它就是不是原子操作。例如下方的代码:
func main() {
a := 0
if a == 0 {
a = 1
}
fmt.Println(a)
}
上方的代码是一个简单的判断分支,尽管代码很少,但也不是原子操作,真正的原子操作是由硬件指令层面支持的。
01.类型
好在大多情况下并不需要自行编写汇编,Go标准库sync/atomic包下已经提供了原子操作相关的API,其提供了以下几种类型以供进行原子操作。
atomic.Bool{}
atomic.Pointer[]{}
atomic.Int32{}
atomic.Int64{}
atomic.Uint32{}
atomic.Uint64{}
atomic.Uintptr{}
atomic.Value{}
其中Pointer原子类型支持泛型,Value类型支持存储任何类型,除此之外,还提供了许多函数来方便操作。因为原子操作的粒度过细,在大多数情况下,更适合处理这些基础的数据类型。
提示:atmoic包下原子操作只有函数签名,没有具体实现,具体的实现是由plan9汇编编写。
02.使用
每一个原子类型都会提供以下三个方法:
Load():原子的获取值
Swap(newVal type) (old type):原子的交换值,并且返回旧值
Store(val type):原子的存储值
---------------------------------------------------------------------------------------------------------
不同的类型可能还会有其他的额外方法,比如整型类型都会提供Add方法来实现原子加减操作。下面以一个int64类型演示为例:
func main() {
var aint64 atomic.Uint64
// 存储值
aint64.Store(64)
// 交换值
aint64.Swap(128)
// 增加
aint64.Add(112)
// 加载值
fmt.Println(aint64.Load())
}
---------------------------------------------------------------------------------------------------------
或者也可以直接使用函数
func main() {
var aint64 int64
// 存储值
atomic.StoreInt64(&aint64, 64)
// 交换值
atomic.SwapInt64(&aint64, 128)
// 增加
atomic.AddInt64(&aint64, 112)
// 加载
fmt.Println(atomic.LoadInt64(&aint64))
}
---------------------------------------------------------------------------------------------------------
其他的类型的使用也是十分类似的,最终输出为:
240
03.CAS
atmoic包还提供了CompareAndSwap操作,也就是CAS,它是乐观锁的一种典型实现。乐观锁本身并不是锁,
是一种并发条件下无锁化并发控制方式。之所以被称作乐观锁,是因为它总是乐观的假设共享数据不会被修改,
仅当发现数据未被修改时才会去执行对应操作,而前面了解到的互斥量就是悲观锁,互斥量总是悲观的认为共享数据肯定会被修改,
所以在操作时会加锁,操作完毕后就会解锁。由于无锁化实现的并发安全效率相对于锁要高一些,
许多并发安全的数据结构都采用了cAS来进行实现,不过真正的效率要结合具体使用场景来看。看下面的一个例子:
---------------------------------------------------------------------------------------------------------
var lock sync.Mutex
var count int
func Add(num int) {
lock.Lock()
count += num
lock.Unlock()
}
---------------------------------------------------------------------------------------------------------
这是一个使用互斥锁的例子,每次增加数字前都会先上锁,执行完毕后就会解锁,过程中会导致其他协程阻塞,
接下来使用CAS改造一下:
var count int64
func Add(num int64) {
for {
expect := atomic.LoadInt64(&count)
if atomic.CompareAndSwapInt64(&count, expect, expect+num) {
break
}
}
}
---------------------------------------------------------------------------------------------------------
对于CAS而言,有三个参数,内存值,期望值,新值。执行时,CAS会将期望值与当前内存值进行比较,
如果内存值与期望值相同,就会执行后续的操作,否则的话什么也不做。对于Go中atomic包下的原子操作,
CAS相关的函数则需要传入地址,期望值,新值,且会返回是否成功替换的布尔值。例如int64类型的CAS 操作函数签名如下:
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
---------------------------------------------------------------------------------------------------------
在CAS的例子中,首先会通过LoadInt64来获取期望值,随后使用CompareAndSwapInt64来进行比较交换,
如果不成功的话就不断循环,直到成功。这样无锁化的操作虽然不会导致协程阻塞,
但是不断的循环对于CPU而言依旧是一个不小的开销,所以在一些实现中失败达到了一定次数可能会放弃操作。
但是对于上面的操作而言,仅仅只是简单的数字相加,涉及到的操作并不复杂,所以完全可以考虑无锁化实现。
---------------------------------------------------------------------------------------------------------
提示
大多数情况下,仅仅只是比较值是无法做到并发安全的,比如因CAS引起ABA问题,就需要使用额外加入version来解决问题。
04.Value
atomic.Value结构体,可以存储任意类型的值,结构体如下
type Value struct {
// any类型
v any
}
---------------------------------------------------------------------------------------------------------
尽管可以存储任意类型,但是它不能存储nil,并且前后存储的值类型应当一致,下面两个例子都无法通过编译
func main() {
var val atomic.Value
val.Store(nil)
fmt.Println(val.Load())
}
// panic: sync/atomic: store of nil value into Value
func main() {
var val atomic.Value
val.Store("hello world")
val.Store(114154)
fmt.Println(val.Load())
}
// panic: sync/atomic: store of inconsistently typed value into Value
---------------------------------------------------------------------------------------------------------
除此之外,它的使用与其他的原子类型并无太大的差别,并且需要注意的是,所有的原子类型都不应该复制值,
而是应该使用它们的指针。
2 并发编程2
2.1 Goroutine
00.介绍
在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?
Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。
01.使用goroutine
Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
02.启动单个goroutine
启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。
举个例子如下:
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
hello()
fmt.Println("main goroutine done!")
}
这个示例中hello函数和下面的语句是串行的,执行的结果是打印完Hello Goroutine!后打印main goroutine done!。
---------------------------------------------------------------------------------------------------------
接下来我们在调用hello函数前面加上关键字go,也就是启动一个goroutine去执行hello这个函数。
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
}
这一次的执行结果只打印了main goroutine done!,并没有打印Hello Goroutine!。为什么呢?
---------------------------------------------------------------------------------------------------------
在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。
当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,main函数所在的goroutine就像是权利的游戏中的夜王,其他的goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。
所以我们要想办法让main函数等一等hello函数,最简单粗暴的方式就是time.Sleep了。
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
time.Sleep(time.Second)
}
执行上面的代码你会发现,这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine!。
首先为什么会先打印main goroutine done!是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的。
03.启动多个goroutine
在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine。让我们再来一个例子: (这里使用了sync.WaitGroup来实现goroutine的同步)
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。
---------------------------------------------------------------------------------------------------------
注意
如果主协程退出了,其他任务还执行吗(运行下面的代码测试一下吧)
package main
import (
"fmt"
"time"
)
func main() {
// 合起来写
go func() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(time.Second)
}
}()
i := 0
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(time.Second)
if i == 2 {
break
}
}
}
04.goroutine与线程
a.可增长的栈
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈
(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,
虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。
b.goroutine调度
GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
1.G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
2.P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
3.M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。
-----------------------------------------------------------------------------------------------------
P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。
2.2 runtime包
01.runtime.Gosched()
让出CPU时间片,重新等待安排任务(大概意思就是本来计划的好好的周末出去烧烤,但是你妈让你去相亲,两种情况第一就是你相亲速度非常快,
见面就黄不耽误你继续烧烤,第二种情况就是你相亲速度特别慢,见面就是你侬我侬的,耽误了烧烤,但是还馋就是耽误了烧烤你还得去烧烤)
package main
import (
"fmt"
"runtime"
)
func main() {
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("world")
// 主协程
for i := 0; i < 2; i++ {
// 切一下,再次分配任务
runtime.Gosched()
fmt.Println("hello")
}
}
02.runtime.Goexit()
退出当前协程(一边烧烤一边相亲,突然发现相亲对象太丑影响烧烤,果断让她滚蛋,然后也就没有然后了)
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
// 结束协程
runtime.Goexit()
defer fmt.Println("C.defer")
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
}
}
03.runtime.GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
---------------------------------------------------------------------------------------------------------
我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
}
---------------------------------------------------------------------------------------------------------
两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下。
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
---------------------------------------------------------------------------------------------------------
Go语言中的操作系统线程和goroutine的关系:
1.一个操作系统线程对应用户态多个goroutine。
2.go程序可以同时使用多个操作系统线程。
3.goroutine和OS线程是多对多的关系,即m:n。
2.3 Channel
01.channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
02.channel类型
channel是一种类型,一种引用类型。声明通道类型的格式如下:
var 变量 chan 元素类型
举几个例子:
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
03.创建channel
通道是引用类型,通道类型的空值是nil。
var ch chan int
fmt.Println(ch) // <nil>
声明的通道后需要使用make函数初始化之后才能使用。
---------------------------------------------------------------------------------------------------------
创建channel的格式如下:
make(chan 元素类型, [缓冲大小])
channel的缓冲大小是可选的。
---------------------------------------------------------------------------------------------------------
举几个例子:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
04.channel操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-符号。
---------------------------------------------------------------------------------------------------------
现在我们先使用以下语句定义一个通道:
ch := make(chan int)
---------------------------------------------------------------------------------------------------------
发送
将一个值发送到通道中。
ch <- 10 // 把10发送到ch中
---------------------------------------------------------------------------------------------------------
接收
从一个通道中接收值。
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
---------------------------------------------------------------------------------------------------------
关闭
我们通过调用内置的close函数来关闭通道。
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
---------------------------------------------------------------------------------------------------------
关闭后的通道有以下特点:
1.对一个关闭的通道再发送值就会导致panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致panic。
05.无缓冲的通道
无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
---------------------------------------------------------------------------------------------------------
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
.../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54
---------------------------------------------------------------------------------------------------------
为什么会出现deadlock错误呢?
因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?
一种方法是启用一个goroutine去接收值,例如:
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}
无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。
06.有缓冲的通道
解决上面问题的方法还有一种就是使用有缓冲区的通道。
我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。
07.close()
可以通过内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main结束")
}
08.如何优雅的从通道循环取值
当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?
我们来看下面这个例子:
// channel 练习
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
}
从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是for range的方式。
09.单向通道
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
Go语言中提供了单向通道来处理这种情况。例如,我们把上面的例子改造如下:
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
其中,
1.chan<- int是一个只能发送的通道,可以发送但是不能接收;
2.<-chan int是一个只能接收的通道,可以接收但是不能发送。
在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。
2.4 Goroutine池
01.介绍
worker pool(goroutine池)
本质上是生产者消费者模型
可以有效控制goroutine数量,防止暴涨
02.需求:
计算一个数字的各个位数之和,例如数字123,结果为1+2+3=6
随机生成数字进行计算
package main
import (
"fmt"
"math/rand"
)
type Job struct {
// id
Id int
// 需要计算的随机数
RandNum int
}
type Result struct {
// 这里必须传对象实例
job *Job
// 求和
sum int
}
func main() {
// 需要2个管道
// 1.job管道
jobChan := make(chan *Job, 128)
// 2.结果管道
resultChan := make(chan *Result, 128)
// 3.创建工作池
createPool(64, jobChan, resultChan)
// 4.开个打印的协程
go func(resultChan chan *Result) {
// 遍历结果管道打印
for result := range resultChan {
fmt.Printf("job id:%v randnum:%v result:%d\n", result.job.Id,
result.job.RandNum, result.sum)
}
}(resultChan)
var id int
// 循环创建job,输入到管道
for {
id++
// 生成随机数
r_num := rand.Int()
job := &Job{
Id: id,
RandNum: r_num,
}
jobChan <- job
}
}
// 创建工作池
// 参数1:开几个协程
func createPool(num int, jobChan chan *Job, resultChan chan *Result) {
// 根据开协程个数,去跑运行
for i := 0; i < num; i++ {
go func(jobChan chan *Job, resultChan chan *Result) {
// 执行运算
// 遍历job管道所有数据,进行相加
for job := range jobChan {
// 随机数接过来
r_num := job.RandNum
// 随机数每一位相加
// 定义返回值
var sum int
for r_num != 0 {
tmp := r_num % 10
sum += tmp
r_num /= 10
}
// 想要的结果是Result
r := &Result{
job: job,
sum: sum,
}
//运算结果扔到管道
resultChan <- r
}
}(jobChan, resultChan)
}
}
2.5 定时器
01.Timer:时间到了,执行只执行1次
package main
import (
"fmt"
"time"
)
func main() {
// 1.timer基本使用
//timer1 := time.NewTimer(2 * time.Second)
//t1 := time.Now()
//fmt.Printf("t1:%v\n", t1)
//t2 := <-timer1.C
//fmt.Printf("t2:%v\n", t2)
// 2.验证timer只能响应1次
//timer2 := time.NewTimer(time.Second)
//for {
// <-timer2.C
// fmt.Println("时间到")
//}
// 3.timer实现延时的功能
//(1)
//time.Sleep(time.Second)
//(2)
//timer3 := time.NewTimer(2 * time.Second)
//<-timer3.C
//fmt.Println("2秒到")
//(3)
//<-time.After(2*time.Second)
//fmt.Println("2秒到")
// 4.停止定时器
//timer4 := time.NewTimer(2 * time.Second)
//go func() {
// <-timer4.C
// fmt.Println("定时器执行了")
//}()
//b := timer4.Stop()
//if b {
// fmt.Println("timer4已经关闭")
//}
// 5.重置定时器
timer5 := time.NewTimer(3 * time.Second)
timer5.Reset(1 * time.Second)
fmt.Println(time.Now())
fmt.Println(<-timer5.C)
for {
}
}
02.Ticker:时间到了,多次执行
package main
import (
"fmt"
"time"
)
func main() {
// 1.获取ticker对象
ticker := time.NewTicker(1 * time.Second)
i := 0
// 子协程
go func() {
for {
//<-ticker.C
i++
fmt.Println(<-ticker.C)
if i == 5 {
//停止
ticker.Stop()
}
}
}()
for {
}
}
2.6 select
01.select多路复用
在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:
for{
// 尝试从ch1接收值
data, ok := <-ch1
// 尝试从ch2接收值
data, ok := <-ch2
…
}
这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。
---------------------------------------------------------------------------------------------------------
select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
02.select可以同时监听一个或多个channel,直到其中一个channel ready
package main
import (
"fmt"
"time"
)
func test1(ch chan string) {
time.Sleep(time.Second * 5)
ch <- "test1"
}
func test2(ch chan string) {
time.Sleep(time.Second * 2)
ch <- "test2"
}
func main() {
// 2个管道
output1 := make(chan string)
output2 := make(chan string)
// 跑2个子协程,写数据
go test1(output1)
go test2(output2)
// 用select监控
select {
case s1 := <-output1:
fmt.Println("s1=", s1)
case s2 := <-output2:
fmt.Println("s2=", s2)
}
}
03.如果多个channel同时ready,则随机选择一个执行
package main
import (
"fmt"
)
func main() {
// 创建2个管道
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
go func() {
//time.Sleep(2 * time.Second)
int_chan <- 1
}()
go func() {
string_chan <- "hello"
}()
select {
case value := <-int_chan:
fmt.Println("int:", value)
case value := <-string_chan:
fmt.Println("string:", value)
}
fmt.Println("main结束")
}
04.可以用于判断管道是否存满
package main
import (
"fmt"
"time"
)
// 判断管道有没有存满
func main() {
// 创建管道
output1 := make(chan string, 10)
// 子协程写数据
go write(output1)
// 取数据
for s := range output1 {
fmt.Println("res:", s)
time.Sleep(time.Second)
}
}
func write(ch chan string) {
for {
select {
// 写数据
case ch <- "hello":
fmt.Println("write hello")
default:
fmt.Println("channel full")
}
time.Sleep(time.Millisecond * 500)
}
}
2.7 并发安全和锁
00.并发安全和锁
有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。
举个例子:
var x int64
var wg sync.WaitGroup
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)
}
上面的代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。
01.互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。
02.读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
读写锁示例:
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
// lock.Lock() // 加互斥锁
rwlock.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwlock.Unlock() // 解写锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func read() {
// lock.Lock() // 加互斥锁
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解读锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。
2.8 Sync
01.sync.WaitGroup
在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:
---------------------------------------------------------------------------------------------------------
方法名 功能
(wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。
每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,
当计数器值为0时,表示所有并发任务已经完成。
---------------------------------------------------------------------------------------------------------
我们利用sync.WaitGroup将上面的代码优化一下:
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是一个结构体,传递的时候要传递指针。
02.sync.Once
a.介绍
说在前面的话:这是一个进阶知识点。
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。
sync.Once只有一个Do方法,其签名如下:
func (o *Once) Do(f func()) {}
注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。
b.加载配置文件示例
延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。我们来看一个例子:
var icons map[string]image.Image
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 被多个goroutine调用时不是并发安全的
func Icon(name string) image.Image {
if icons == nil {
loadIcons()
}
return icons[name]
}
-----------------------------------------------------------------------------------------------------
多个goroutine并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重排为以下结果:
func loadIcons() {
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}
在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做又会引发性能问题。
-----------------------------------------------------------------------------------------------------
使用sync.Once改造的示例代码如下:
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]
}
sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
03.sync.Map
Go语言中内置的map不是并发安全的。请看下面的示例:
var m = make(map[string]int)
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 < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}
上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。
---------------------------------------------------------------------------------------------------------
像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
2.9 原子操作(atomic包)
00.原子操作
代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。
针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,
因此性能比加锁操作更好。Go语言中原子操作由内置的标准库sync/atomic提供。
01.atomic包
方法 解释
func LoadInt32(addr int32) (val int32)
func LoadInt64(addr `int64) (val int64)<br>func LoadUint32(addruint32) (val uint32)<br>func LoadUint64(addruint64) (val uint64)<br>func LoadUintptr(addruintptr) (val uintptr)<br>func LoadPointer(addrunsafe.Pointer`) (val unsafe.Pointer) 读取操作
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) 写入操作
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) 修改操作
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) 交换操作
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
02.示例
我们填写一个示例来比较下互斥锁和原子操作的性能。
var x int64
var l sync.Mutex
var wg sync.WaitGroup
// 普通版加函数
func add() {
// x = x + 1
x++ // 等价于上面的操作
wg.Done()
}
// 互斥锁版加函数
func mutexAdd() {
l.Lock()
x++
l.Unlock()
wg.Done()
}
// 原子操作版加函数
func atomicAdd() {
atomic.AddInt64(&x, 1)
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10000; i++ {
wg.Add(1)
// go add() // 普通版add函数 不是并发安全的
// go mutexAdd() // 加锁版add函数 是并发安全的,但是加锁性能开销大
go atomicAdd() // 原子操作版add函数 是并发安全,性能优于加锁版
}
wg.Wait()
end := time.Now()
fmt.Println(x)
fmt.Println(end.Sub(start))
}
atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。
2.10 爬虫小案例
01.爬虫步骤
明确目标(确定在哪个网站搜索)
爬(爬下内容)
取(筛选想要的)
处理数据(按照你的想法去处理)
---------------------------------------------------------------------------------------------------------
package main
import (
"fmt"
"io/ioutil"
"net/http"
"regexp"
)
//这个只是一个简单的版本只是获取QQ邮箱并且没有进行封装操作,另外爬出来的数据也没有进行去重操作
var (
// \d是数字
reQQEmail = `(\d+)@qq.com`
)
// 爬邮箱
func GetEmail() {
// 1.去网站拿数据
resp, err := http.Get("https://tieba.baidu.com/p/6051076813?red_tag=1573533731")
HandleError(err, "http.Get url")
defer resp.Body.Close()
// 2.读取页面内容
pageBytes, err := ioutil.ReadAll(resp.Body)
HandleError(err, "ioutil.ReadAll")
// 字节转字符串
pageStr := string(pageBytes)
//fmt.Println(pageStr)
// 3.过滤数据,过滤qq邮箱
re := regexp.MustCompile(reQQEmail)
// -1代表取全部
results := re.FindAllStringSubmatch(pageStr, -1)
//fmt.Println(results)
// 遍历结果
for _, result := range results {
fmt.Println("email:", result[0])
fmt.Println("qq:", result[1])
}
}
// 处理异常
func HandleError(err error, why string) {
if err != nil {
fmt.Println(why, err)
}
}
func main() {
GetEmail()
}
02.正则表达式
文档:https://studygolang.com/pkgdoc
API
re := regexp.MustCompile(reStr),传入正则表达式,得到正则表达式对象
ret := re.FindAllStringSubmatch(srcStr,-1):用正则对象,获取页面页面,srcStr是页面内容,-1代表取全部
爬邮箱
方法抽取
爬超链接
爬手机号 http://www.zhaohaowang.com/ 如果连接失效了自己找一个有手机号的就好了
爬身份证号 http://henan.qq.com/a/20171107/069413.htm 如果连接失效了自己找一个就好了
爬图片链接
---------------------------------------------------------------------------------------------------------
package main
import (
"fmt"
"io/ioutil"
"net/http"
"regexp"
)
var (
// w代表大小写字母+数字+下划线
reEmail = `\w+@\w+\.\w+`
// s?有或者没有s
// +代表出1次或多次
//\s\S各种字符
// +?代表贪婪模式
reLinke = `href="(https?://[\s\S]+?)"`
rePhone = `1[3456789]\d\s?\d{4}\s?\d{4}`
reIdcard = `[123456789]\d{5}((19\d{2})|(20[01]\d))((0[1-9])|(1[012]))((0[1-9])|([12]\d)|(3[01]))\d{3}[\dXx]`
reImg = `https?://[^"]+?(\.((jpg)|(png)|(jpeg)|(gif)|(bmp)))`
)
// 处理异常
func HandleError(err error, why string) {
if err != nil {
fmt.Println(why, err)
}
}
func GetEmail2(url string) {
pageStr := GetPageStr(url)
re := regexp.MustCompile(reEmail)
results := re.FindAllStringSubmatch(pageStr, -1)
for _, result := range results {
fmt.Println(result)
}
}
// 抽取根据url获取内容
func GetPageStr(url string) (pageStr string) {
resp, err := http.Get(url)
HandleError(err, "http.Get url")
defer resp.Body.Close()
// 2.读取页面内容
pageBytes, err := ioutil.ReadAll(resp.Body)
HandleError(err, "ioutil.ReadAll")
// 字节转字符串
pageStr = string(pageBytes)
return pageStr
}
func main() {
// 2.抽取的爬邮箱
// GetEmail2("https://tieba.baidu.com/p/6051076813?red_tag=1573533731")
// 3.爬链接
//GetLink("http://www.baidu.com/s?wd=%E8%B4%B4%E5%90%A7%20%E7%95%99%E4%B8%8B%E9%82%AE%E7%AE%B1&rsv_spt=1&rsv_iqid=0x98ace53400003985&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_enter=1&rsv_dl=ib&rsv_sug2=0&inputT=5197&rsv_sug4=6345")
// 4.爬手机号
//GetPhone("https://www.zhaohaowang.com/")
// 5.爬身份证号
//GetIdCard("https://henan.qq.com/a/20171107/069413.htm")
// 6.爬图片
// GetImg("http://image.baidu.com/search/index?tn=baiduimage&ps=1&ct=201326592&lm=-1&cl=2&nc=1&ie=utf-8&word=%E7%BE%8E%E5%A5%B3")
}
func GetIdCard(url string) {
pageStr := GetPageStr(url)
re := regexp.MustCompile(reIdcard)
results := re.FindAllStringSubmatch(pageStr, -1)
for _, result := range results {
fmt.Println(result)
}
}
// 爬链接
func GetLink(url string) {
pageStr := GetPageStr(url)
re := regexp.MustCompile(reLinke)
results := re.FindAllStringSubmatch(pageStr, -1)
for _, result := range results {
fmt.Println(result[1])
}
}
//爬手机号
func GetPhone(url string) {
pageStr := GetPageStr(url)
re := regexp.MustCompile(rePhone)
results := re.FindAllStringSubmatch(pageStr, -1)
for _, result := range results {
fmt.Println(result)
}
}
func GetImg(url string) {
pageStr := GetPageStr(url)
re := regexp.MustCompile(reImg)
results := re.FindAllStringSubmatch(pageStr, -1)
for _, result := range results {
fmt.Println(result[0])
}
}
03.并发爬取美图
下面的两个是即将要爬的网站,如果网址失效自己换一个就好了
https://www.bizhizu.cn/shouji/tag-%E5%8F%AF%E7%88%B1/1.html
---------------------------------------------------------------------------------------------------------
package main
import (
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
)
func HandleError(err error, why string) {
if err != nil {
fmt.Println(why, err)
}
}
// 下载图片,传入的是图片叫什么
func DownloadFile(url string, filename string) (ok bool) {
resp, err := http.Get(url)
HandleError(err, "http.get.url")
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
HandleError(err, "resp.body")
filename = "E:/topgoer.com/src/github.com/student/3.0/img/" + filename
// 写出数据
err = ioutil.WriteFile(filename, bytes, 0666)
if err != nil {
return false
} else {
return true
}
}
// 并发爬思路:
// 1.初始化数据管道
// 2.爬虫写出:26个协程向管道中添加图片链接
// 3.任务统计协程:检查26个任务是否都完成,完成则关闭数据管道
// 4.下载协程:从管道里读取链接并下载
var (
// 存放图片链接的数据管道
chanImageUrls chan string
waitGroup sync.WaitGroup
// 用于监控协程
chanTask chan string
reImg = `https?://[^"]+?(\.((jpg)|(png)|(jpeg)|(gif)|(bmp)))`
)
func main() {
// myTest()
// DownloadFile("http://i1.shaodiyejin.com/uploads/tu/201909/10242/e5794daf58_4.jpg", "1.jpg")
// 1.初始化管道
chanImageUrls = make(chan string, 1000000)
chanTask = make(chan string, 26)
// 2.爬虫协程
for i := 1; i < 27; i++ {
waitGroup.Add(1)
go getImgUrls("https://www.bizhizu.cn/shouji/tag-%E5%8F%AF%E7%88%B1/" + strconv.Itoa(i) + ".html")
}
// 3.任务统计协程,统计26个任务是否都完成,完成则关闭管道
waitGroup.Add(1)
go CheckOK()
// 4.下载协程:从管道中读取链接并下载
for i := 0; i < 5; i++ {
waitGroup.Add(1)
go DownloadImg()
}
waitGroup.Wait()
}
// 下载图片
func DownloadImg() {
for url := range chanImageUrls {
filename := GetFilenameFromUrl(url)
ok := DownloadFile(url, filename)
if ok {
fmt.Printf("%s 下载成功\n", filename)
} else {
fmt.Printf("%s 下载失败\n", filename)
}
}
waitGroup.Done()
}
// 截取url名字
func GetFilenameFromUrl(url string) (filename string) {
// 返回最后一个/的位置
lastIndex := strings.LastIndex(url, "/")
// 切出来
filename = url[lastIndex+1:]
// 时间戳解决重名
timePrefix := strconv.Itoa(int(time.Now().UnixNano()))
filename = timePrefix + "_" + filename
return
}
// 任务统计协程
func CheckOK() {
var count int
for {
url := <-chanTask
fmt.Printf("%s 完成了爬取任务\n", url)
count++
if count == 26 {
close(chanImageUrls)
break
}
}
waitGroup.Done()
}
// 爬图片链接到管道
// url是传的整页链接
func getImgUrls(url string) {
urls := getImgs(url)
// 遍历切片里所有链接,存入数据管道
for _, url := range urls {
chanImageUrls <- url
}
// 标识当前协程完成
// 每完成一个任务,写一条数据
// 用于监控协程知道已经完成了几个任务
chanTask <- url
waitGroup.Done()
}
// 获取当前页图片链接
func getImgs(url string) (urls []string) {
pageStr := GetPageStr(url)
re := regexp.MustCompile(reImg)
results := re.FindAllStringSubmatch(pageStr, -1)
fmt.Printf("共找到%d条结果\n", len(results))
for _, result := range results {
url := result[0]
urls = append(urls, url)
}
return
}
// 抽取根据url获取内容
func GetPageStr(url string) (pageStr string) {
resp, err := http.Get(url)
HandleError(err, "http.Get url")
defer resp.Body.Close()
// 2.读取页面内容
pageBytes, err := ioutil.ReadAll(resp.Body)
HandleError(err, "ioutil.ReadAll")
// 字节转字符串
pageStr = string(pageBytes)
return pageStr
}