三、Go并发
1、go协程
golang中的主线程:(可以理解为线程/也可以理解为进程),在一个Golang程
序的主线程上可以起多个协程。Golang中多协程可以实现并行或者并发。
多协程和多线程:Golang中每个goroutine(协程)默认占用内存远比Java、C的线程少。
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB左右),一个goroutine(协程)占用内存非常小,只有2KB左右,多协程goroutine切换调度开销方面远比线程要少。
协程:
可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的。Golang的一大特色就是从语言层面原生持协程,在函数或者方法前面加go关键字就可创建一个协程。可以说Golang中的协程就是goroutine。
// 协程需要运行的方法 func test() { for i := 0; i < 5; i++ { fmt.Println("test 你好golang") time.Sleep(time.Millisecond * 100) } } func main() { // 通过go关键字,就可以直接开启一个协程 go test() // 这是主进程执行的 for i := 0; i < 5; i++ { fmt.Println("main 你好golang") time.Sleep(time.Millisecond * 100) } }
上述的代码其实还有问题的,也就是说当主进程执行完毕后,不管协程有没有执行完成,都会退出,需要用到 sync.WaitGroup等待协程
协程计数器使用:
// 定义一个协程计数器 var wg sync.WaitGroup // 开启协程,协程计数器加1 wg.Add(1) // 协程计数器减1 wg.Done()
实现代码:
// 定义一个协程计数器 var wg sync.WaitGroup func test() { // 这是主进程执行的 for i := 0; i < 1000; i++ { fmt.Println("test1 你好golang", i) //time.Sleep(time.Millisecond * 100) } // 协程计数器减1 wg.Done() } func test2() { // 这是主进程执行的 for i := 0; i < 1000; i++ { fmt.Println("test2 你好golang", i) //time.Sleep(time.Millisecond * 100) } // 协程计数器减1 wg.Done() } func main() { // 通过go关键字,就可以直接开启一个协程 wg.Add(1) go test() // 协程计数器加1 wg.Add(1) go test2() // 这是主进程执行的 for i := 0; i < 1000; i++ { fmt.Println("main 你好golang", i) //time.Sleep(time.Millisecond * 100) } // 等待所有的协程执行完毕 wg.Wait() fmt.Println("主线程退出") }
设置Go并行运行的时候占用的cpu数量:
func main() { // 获取cpu个数 npmCpu := runtime.NumCPU() fmt.Println("cup的个数:", npmCpu) // 设置允许使用的CPU数量 runtime.GOMAXPROCS(runtime.NumCPU() - 1) }
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
2、chan管道
管道是Golang在语言级别上提供的goroutine间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。
Golang的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
Go语言中的管道(channel)是一种特殊的类型。管道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个管道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel是一种引用类型,使用如下:
// 声明一个传递整型的管道 var ch1 chan int // 声明一个传递int切片的管道 var ch3 chan []int // 创建一个能存储10个int类型的数据管道 ch1 = make(chan int, 10) // 创建一个能存储3个[]int切片类型的管道 ch3 = make(chan []int, 3) // 把10发送到ch中 ch <- 10 // 从管道ch中获取值 x := <- ch //关闭管道资源 close(ch)
// 创建管道 ch := make(chan int, 3) // 给管道里面存储数据 ch <- 10 ch <- 21 ch <- 32 // 获取管道里面的内容 a := <- ch fmt.Println("打印出管道的值:", a) fmt.Println("打印出管道的值:", <- ch) fmt.Println("打印出管道的值:", <- ch) // 管道的值、容量、长度 fmt.Printf("地址:%v 容量:%v 长度:%v \n", ch, cap(ch), len(ch)) // 管道的类型 fmt.Printf("%T \n", ch) // 管道阻塞(当没有数据的时候取,会出现阻塞,同时当管道满了,继续存也会) <- ch // 没有数据取,出现阻塞 ch <- 10 ch <- 10 ch <- 10 ch <- 10 // 管道满了,继续存,也出现阻塞
for range从管道循环取值:
当管道被关闭时,再往该管道发送值会引发panic;当管道被关闭时,从管道中取值,会一直取,直到没有返回零值
// for range循环遍历管道的值(管道没有key) for value := range ch { fmt.Println(value) }
注:使用for range遍历的时候,一定在之前需要先关闭管道,否则会发生死锁,for i的循环方式,可以不关闭管道
size := len(ch) for i := 0; i < size; i++ { fmt.Println(<-ch) }
案例1:定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步进行
func write(ch chan int) { for i := 0; i < 10; i++ { fmt.Println("写入:", i) ch <- i time.Sleep(time.Microsecond * 10) } wg.Done() } func read(ch chan int) { for i := 0; i < 10; i++ { fmt.Println("读取:", <- ch) time.Sleep(time.Microsecond * 10) } wg.Done() } var wg sync.WaitGroup func main() { ch := make(chan int, 10) wg.Add(1) go write(ch) wg.Add(1) go read(ch) // 等待 wg.Wait() fmt.Println("主线程执行完毕") }
注:管道是安全的,是一边写入,一边读取,当读取比较快的时候,会等待写入
案例2:goroutine 结合 channel打印素数
// 想intChan中放入 1~ 120000个数 func putNum(intChan chan int) { for i := 2; i < 120000; i++ { intChan <- i } wg.Done() close(intChan) } // cong intChan取出数据,并判断是否为素数,如果是的话,就把得到的素数放到primeChan中 func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) { for value := range intChan { for i := 2; i <= int(math.Sqrt(float64(value))); i++ { if i%i == 0 { continue } } primeChan <- value } exitChan <- true wg.Done() } // 打印素数 func printPrime(primeChan chan int) { for value := range primeChan { fmt.Println(value) } wg.Done() } var wg sync.WaitGroup func main() { // 写入数字 intChan := make(chan int, 1000) // 存放素数 primeChan := make(chan int, 1000) // 存放 primeChan退出状态 exitChan := make(chan bool, 16) // 开启写值的协程 go putNum(intChan) // 开启计算素数的协程 for i := 0; i < 10; i++ { wg.Add(1) go primeNum(intChan, primeChan, exitChan) } // 开启打印的协程 wg.Add(1) go printPrime(primeChan) // 匿名自运行函数 wg.Add(1) go func() { for i := 0; i < 10; i++ { // 如果exitChan 没有完成10次遍历,将会等待 <-exitChan } // 关闭primeChan close(primeChan) wg.Done() }() wg.Wait() fmt.Println("主线程执行完毕") }
3、单向管道
有时候管道会作为参数在多个任务函数间传递,在不同的任务函数中,使用管道都会对其进行限制,比如限制管道在函数中只能发送或者只能接受,而默认的管道是 可读可写
// 定义一种可读可写的管道 var ch = make(chan int, 2) ch <- 10 <- ch // 管道声明为只写管道,只能够写入,不能读 var ch2 = make(chan<- int, 2) ch2 <- 10 // 声明一个只读管道 var ch3 = make(<-chan int, 2) <- ch3
4、Select多路复用
在某些场景下我们需要同时从多个通道接收数据。这个时候就可以用到golang中给我们提供的select多路复用,,可以同时响应多个管道的操作。
select的使用类似于switch 语句,它有一系列case分支和一个默认的分支。每个case会对应一个管道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:
intChan := make(chan int, 10) intChan <- 10 intChan <- 12 intChan <- 13 stringChan := make(chan int, 10) stringChan <- 20 stringChan <- 23 stringChan <- 24 // 每次循环的时候,会随机中一个chan中读取,其中for是死循环 for { select { case v:= <- intChan: fmt.Println("从initChan中读取数据:", v) case v:= <- stringChan: fmt.Println("从stringChan中读取数据:", v) default: fmt.Println("所有的数据获取完毕") return } }
注:使用select来获取数据的时候,不需要关闭chan,不然会出现问题
go协程发生panic捕获异常:
// 捕获异常 defer func() { if err := recover(); err != nil { fmt.Println("errTest发生错误") } }()
5、协程互斥
互斥锁:
互斥锁是传统并发编程中对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock 进行解锁
// 定义一个锁 var mutex sync.Mutex // 协程访问共享资源前,先获取锁资源,进行加锁 mutex.Lock() // 访问完后释放锁资源 mutex.Unlock()
读写互斥锁:
互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。
其实,当我们对一个不会变化的数据只做“读”操作的话,是不存在资源竞争的问题的。因为数据是不变的,不管怎么读取,多少goroutine同时读取,都是可以的。
所以问题不是出在“读”上,主要是修改,也就是“写”。修改的数据要同步,这样其他goroutine才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。
因此,衍生出另外一种锁,叫做读写锁。
读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。
GO中的读写锁由结构体类型sync.RWMutex表示。此类型的方法集合中包含两对方法
四、反射
1、反射
有时我们需要写一个函数,这个函数有能力统一处理各种值类型,而这些类型可能无法共享同一个接口,也可能布局未知,也有可能这个类型在我们设计函数时还不存在,这个时候我们就可以用到反射。
空接口可以存储任意类型的变量,那我们如何知道这个空接口保存数据的类型是什么? 值是什么呢?
- 可以使用类型断言
- 可以使用反射实现,也就是在程序运行时动态的获取一个变量的类型信息和值信息。
把结构体序列化成json字符串,自定义结构体Tab标签的时候就用到了反射
后面所说的ORM框架,底层就是用到了反射技术
ORM:对象关系映射(Object Relational Mapping,简称 ORM)是通过使用描述对象和数据库之间的映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。
反射介绍:
反射是指在程序运行期间对程序本身进行访问和修改的能力。正常情况程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
反射的功能:
- 反射可以在程序运行期间动态的获取变量的各种信息,比如变量的类型类别
- 如果是结构体,通过反射还可以获取结构体本身的信息,比如结构体的字段、结构体的方法。
- 通过反射,可以修改变量的值,可以调用关联的方法
变量是分为两部分的:
- 类型信息:预先定义好的元信息。
- 值信息:程序运行过程中可动态变化的。
反射使用:
在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value两部分组成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个重要函数来获取任意对象的Value 和 Type
使用reflect.TypeOf()函数可以接受任意参数,可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息
v := reflect.TypeOf(x) fmt.Println(v)
反射修改变量的值:
func main() { i := 1 v := reflect.ValueOf(&i)//传入地址才能修改变量 v.Elem().SetInt(10)//Elem()获取指针指向的变量 SetInt()修改值 fmt.Println(i) }
2、Name和Kind
在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kid)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。
v := reflect.TypeOf(x) fmt.Println("类型 ", v) fmt.Println("类型名称 ", v.Name()) fmt.Println("类型种类 ", v.Kind())
反射和switch:
// 通过反射来获取变量的原始值 v := reflect.ValueOf(x) // 获取种类 kind := v.Kind() switch kind { case reflect.Int: fmt.Println("我是int类型") case reflect.Float64: fmt.Println("我是float64类型") default: fmt.Println("我是其它类型") }
reflect.ValueOf() 返回的是reflect.Value类型,其中包含了原始值的值信息,reflect.Value与原始值之间可以互相转换:
方法 | 说明 |
interface{} | 将值以interface{}类型返回,可以通过类型断言转换为指定类型 |
Int() int64 | 将值以int类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 | 将值以uint类型返回,所有无符号整型均可以以此方式返回 |
Float() float64 | 将值以双精度(float 64)类型返回,所有浮点数(float 32、float64)均可以以此方式返回 |
3、结构体反射
任意值通过reflect.Typeof)获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()和Field()方法获得结构体成员的详细信息。
获取结构体成员相关的的方法:
方法 | 说明 |
Field(i int)StructField | 根据索引,返回索引对应的结构体字段的信息 |
NumField() int | 返回结构体成员字段数量 |
FieldByName(name string)(StructField, bool) | 根据给定字符串返回字符串赌赢的结构体字段信息 |
FieldByIndex(index []int)StructField | 多层成员访问时,根据[] int 提供的每个结构 |
type Student4 struct { Name string `json: "name"` Age int `json: "age"` Score int `json: "score"` } func (s Student4) GetInfo() string { var str = fmt.Sprintf("姓名:%v 年龄:%v 成绩:%v", s.Name, s.Age, s.Score) return str } func (s *Student4) SetInfo(name string, age int, score int) { s.Name = name s.Age = age s.Score = score } func (s Student4) PrintStudent() { fmt.Println("打印学生: name#", s.Name, " age#", s.Age, " score#", s.Score) } func PrintStructFn(s interface{}) { t := reflect.TypeOf(s) // 判断传递过来的是否是结构体 if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct { fmt.Println("请传入结构体类型!") return } // 通过类型变量里面的Method,可以获取结构体的方法 method0 := t.Method(0) // 获取第一个方法, 这个是和ACSII相关 fmt.Println(method0.Name) // 通过类型变量获取这个结构体有多少方法 methodCount := t.NumMethod() fmt.Println("拥有的方法", methodCount) // 通过值变量 执行方法(注意需要使用值变量,并且要注意参数) v := reflect.ValueOf(s) // 通过值变量来获取参数 v.MethodByName("PrintStudent").Call(nil) // 手动传参 var params = make([]reflect.Value, 3) params[0] = reflect.ValueOf("张三") params[1] = reflect.ValueOf(23) params[2] = reflect.ValueOf(99) v.MethodByName("SetInfo").Call(params) v.MethodByName("PrintStudent").Call(nil) } func main() { s := Student4{} PrintStructFn(&s) }
注:只有公有函数(首字母大写的函数)可以通过reflect.MethodByName()函数获取,私有方法是不行的。
4、反射的优劣
反射的好处:
- 为了降低多写代码造成的bug率,做更好的归约和抽象
- 为了灵活、好用、方便,做动态解析、调用和处理
- 为了代码好看、易读、提高开发效率,补足与动态语言之间的一些差别
反射的弊端:
- 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
- Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
- 反射对性能影响还是比较大的, 比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性
rintln(“拥有的方法”, methodCount) // 通过值变量 执行方法(注意需要使用值变量,并且要注意参数) v := reflect.ValueOf(s) // 通过值变量来获取参数 v.MethodByName(“PrintStudent”).Call(nil) // 手动传参 var params = make([]reflect.Value, 3) params[0] = reflect.ValueOf(“张三”) params[1] = reflect.ValueOf(23) params[2] = reflect.ValueOf(99) v.MethodByName(“SetInfo”).Call(params) v.MethodByName(“PrintStudent”).Call(nil) } func main() { s := Student4{} PrintStructFn(&s) }
[外链图片转存中...(img-lCRUAbuc-1698414788807)] 注:只有公有函数(首字母大写的函数)可以通过reflect.MethodByName()函数获取,私有方法是不行的。 ### 反射的优劣 反射的好处: 1. 为了降低多写代码造成的bug率,做更好的归约和抽象 2. 为了灵活、好用、方便,做动态解析、调用和处理 3. 为了代码好看、易读、提高开发效率,补足与动态语言之间的一些差别 反射的弊端: 1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。 2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。 3. 反射对性能影响还是比较大的, 比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性