反射
反射
优点: 让代码更灵活
缺点: 运行效率低
反射应用
各种 web框架, 配置文件解析库,ORM框架
反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。
在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反
射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
Go程序在运行期使用reflect包访问程序的反射信息。
在上一篇博客中我们介绍了空接口。 空接口可以存储任意类型的变量,那我们如何知道这个空接口保存的数据是什么呢? 反射就是在运行时
动态的获取一个变量的类型信息和值信息。
reflect包 // 反射包 reflect.TypeOf // 拿到的是动态类型 reflect.ValueOf // 拿到的是动态的值
在Go语言的反射机制中,任何接口值都由是一个具体类型和具体类型的值两部分组成的(我们在上一篇接口的博客中有介绍相关概念)。
在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。
TypeOf
在Go语言中,使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。
package main import ( "fmt" "reflect" ) func reflectType(x interface{}) { v := reflect.TypeOf(x) // 拿到x的动态类型信息 fmt.Printf("type:%v ", v) } func main() { var a float32 = 3.14 reflectType(a) // type:float32 var b int64 = 100 reflectType(b) // type:int64 }
type name 、 type kind
在反射中关于类型还划分为两种:
类型(Type)和种类(Kind)。
因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。
举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。
package main import ( "fmt" "reflect" ) type myInt int64 func reflectType(x interface{}) { t := reflect.TypeOf(x) fmt.Printf("type:%v kind:%v ", t.Name(), t.Kind()) } func main() { var a *float32 // 指针 var b myInt // 自定义类型 var c rune // 类型别名 reflectType(a) // type: kind:ptr reflectType(b) // type:myInt kind:int64 reflectType(c) // type:int32 kind:int32 type person struct { name string age int } type book struct{ title string } var d = person{ name: "hc", age: 18, } var e = book{title: "《add your papa》"} reflectType(d) // type:person kind:struct reflectType(e) // type:book kind:struct }
Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空。
在reflect包中定义的Kind类型如下:
type Kind uint const ( Invalid Kind = iota // 非法类型 Bool // 布尔型 Int // 有符号整型 Int8 // 有符号8位整型 Int16 // 有符号16位整型 Int32 // 有符号32位整型 Int64 // 有符号64位整型 Uint // 无符号整型 Uint8 // 无符号8位整型 Uint16 // 无符号16位整型 Uint32 // 无符号32位整型 Uint64 // 无符号64位整型 Uintptr // 指针 Float32 // 单精度浮点数 Float64 // 双精度浮点数 Complex64 // 64位复数类型 Complex128 // 128位复数类型 Array // 数组 Chan // 通道 Func // 函数 Interface // 接口 Map // 映射 Ptr // 指针 Slice // 切片 String // 字符串 Struct // 结构体 UnsafePointer // 底层指针 )
ValueOf
reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。
reflect.Value类型提供的获取原始值的方法如下:
方法 说明
Interface() interface{} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回 Uint() uint64 将值以 uint 类型返回,所有无符号整型均可以此方式返回 Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 Bool() bool 将值以 bool 类型返回 Bytes() []bytes 将值以字节数组 []bytes 类型返回 String() string 将值以字符串类型返回
通过反射获取值
func reflectValue(x interface{}) { v := reflect.ValueOf(x) k := v.Kind() switch k { case reflect.Int64: // v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换 fmt.Printf("type is int64, value is %d ", int64(v.Int())) case reflect.Float32: // v.Float()从反射中获取整型的原始值,然后通过float32()强制类型转换 fmt.Printf("type is float32, value is %f ", float32(v.Float())) case reflect.Float64: // v.Float()从反射中获取整型的原始值,然后通过float64()强制类型转换 fmt.Printf("type is float64, value is %f ", float64(v.Float())) } } func main() { var a float32 = 3.14 var b int64 = 100 reflectValue(a) // type is float32, value is 3.140000 reflectValue(b) // type is int64, value is 100 // 将int类型的原始值转换为reflect.Value类型 c := reflect.ValueOf(10) fmt.Printf("type c :%T ", c) // type c :reflect.Value }
通过反射设置变量的值
想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的Elem()方
法来获取指针对应的值。
package main import ( "fmt" "reflect" ) func reflectSetValue1(x interface{}) { v := reflect.ValueOf(x) if v.Kind() == reflect.Int64 { v.SetInt(200) //修改的是副本,reflect包会引发panic } } func reflectSetValue2(x interface{}) { v := reflect.ValueOf(x) // 反射中使用 Elem()方法获取指针对应的值 if v.Elem().Kind() == reflect.Int64 { v.Elem().SetInt(200) } } func main() { var a int64 = 100 // reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value reflectSetValue2(&a) fmt.Println(a) }
isNil() 、 isValid()
isNil()
func (v Value) IsNil() bool
IsNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。
isValid()
func (v Value) IsValid() bool
IsValid()返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。
举个例子
IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。
func main() { // *int类型空指针 var a *int fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil()) // nil值 fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) // 实例化一个匿名结构体 b := struct{}{} // 尝试从结构体中查找"abc"字段 fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid()) // 尝试从结构体中查找"abc"方法 fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid()) // map c := map[string]int{} // 尝试从map中查找一个不存在的键 fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid()) }
结构体反射
与结构体相关的方法
任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()和Field()
方法获得结构体成员的详细信息。
reflect.Type中与获取结构体成员相关的的方法如下表所示。
方法 说明 Field(i int) StructField 根据索引,返回索引对应的结构体字段的信息。 NumField() int 返回结构体成员字段数量。 FieldByName(name string) (StructField, bool) 根据给定字符串返回字符串对应的结构体字段的信息。 FieldByIndex(index []int) StructField 多层成员访问时,根据[]int提供的每个结构体的字段索引,返回字段的信息 FieldByNameFunc(match func(string) bool) (StructField,bool) 根据传入的匹配函数匹配需要的字段。 NumMethod() int 返回该类型的方法集中方法的数目 Method(int) Method 返回该类型方法集中的第i个方法 MethodByName(string)(Method, bool) 根据方法名返回该类型方法集中的方法
StructField类型
StructField类型用来描述结构体中的一个字段的信息。
StructField的定义如下:
type StructField struct { // Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。 // 参见http://golang.org/ref/spec#Uniqueness_of_identifiers Name string PkgPath string Type Type // 字段的类型 Tag StructTag // 字段的标签 Offset uintptr // 字段在结构体中的字节偏移量 Index []int // 用于Type.FieldByIndex时的索引切片 Anonymous bool // 是否匿名字段 }
结构体反射示例
当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息。
type student struct { Name string `json:"name"` Score int `json:"score"` } func main() { stu1 := student{ Name: "小王子", Score: 90, } t := reflect.TypeOf(stu1) fmt.Println(t.Name(), t.Kind()) // student struct // 通过for循环遍历结构体的所有字段信息 for i := 0; i < t.NumField(); i++ { field := t.Field(i) fmt.Printf("name:%s index:%d type:%v json tag:%v ", field.Name, field.Index, field.Type, field.Tag.Get("json")) } // 通过字段名获取指定结构体字段信息 if scoreField, ok := t.FieldByName("Score"); ok { fmt.Printf("name:%s index:%d type:%v json tag:%v ", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json")) } }
接下来编写一个函数printMethod(s interface{})来遍历打印s包含的方法。
// 给student添加两个方法 Study和Sleep(注意首字母大写) func (s student) Study() string { msg := "好好学习,天天向上。" fmt.Println(msg) return msg } func (s student) Sleep() string { msg := "好好睡觉,快快长大。" fmt.Println(msg) return msg } func printMethod(x interface{}) { t := reflect.TypeOf(x) v := reflect.ValueOf(x) fmt.Println(t.NumMethod()) for i := 0; i < v.NumMethod(); i++ { methodType := v.Method(i).Type() fmt.Printf("method name:%s ", t.Method(i).Name) fmt.Printf("method:%s ", methodType) // 通过反射调用方法传递的参数必须是 []reflect.Value 类型 var args = []reflect.Value{} v.Method(i).Call(args) } }
反射是把双刃剑
反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。
1. 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后 2. 大量使用反射的代码通常难以理解。 3. 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。
并发
goroutine 协程
channel 管道
并发控制 与 锁
并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。
Go语言中的并发编程
并发与并行
并发 : 同一时间段内同时执行多个任务(你在用微信和两个女朋友聊天)。
并行 : 同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。
进程 : 一个程序启动之后就创建一个进程
线程 : 操作系统调度的最小单位
协程 : 用户态的线程
Go语言的并发通过goroutine实现。
goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。
goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。
Go语言还提供channel在多个goroutine间进行通信。
goroutine和channel是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。
goroutine
在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度
线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮
助我们把这些任务分配到CPU上实现并发执行呢?
Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会
智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下
文切换的机制。
在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需
要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。
使用goroutine -> 一个goroutine必定对应一个函数
Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
启动单个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这个函数。
var wg sync.WaitGroup // 结构体 func hello() { // defer wg.Done() // goroutine结束就登记-1 fmt.Println("Hello Goroutine!", i) wg.Done() // -1 } func main() { defer fmt.Println("ok") // defer 是main函数执行完才执行 defer wg.Add(1) go hello() //1. 创建一个goroutine 2.启动另外一个goroutine去执行hello函数 fmt.Println("main goroutine done!") // time.Sleep(time.Second) // 等hello 执行完 (执行hello函数的那个goroutine执行完) wg.Wait() }
这一次的执行结果只打印了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是继续执行的。
启动多个goroutine
在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine。
让我们再来一个例子: (这里使用了sync.WaitGroup来实现goroutine的同步)
// 利用sync.WaitGroup 实现优雅的等待 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的调度是随机的。
sync.WaitGroup Add() // 计数器 Done() // 计数器-1,最好用defer注册 Wait() // 等待
goroutine与线程
可增长的栈
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),
goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。
所以在Go语言中一次创建十万左右的goroutine也是可以的。
goroutine调度
GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
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调度方面的性能。
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() { defer wg.Done() for i := 1; i < 10; i++ { time.Sleep(time.Millisecond) fmt.Println("A:", i) } } func b() { defer wg.Done() for i := 1; i < 10; i++ { time.Sleep(time.Millisecond*5) fmt.Println("B:", i) } } func main() { runtime.GOMAXPROCS(1) // 设置 程序只用一个逻辑核心 m:n 设置n为1 go a() go b() wg.Wait() // 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。 m 是goroutine 的数量,n 是干活的线程数量
channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。
为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。
channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。
通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel类型
channel是一种类型,一种引用类型。声明通道类型的格式如下:
var 变量 chan 元素类型
举几个例子:
var ch1 chan int // 声明一个传递整型的通道 var ch2 chan bool // 声明一个传递布尔型的通道 var ch3 chan []int // 声明一个传递int切片的通道
创建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)
channel 操作 ,通道的操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用 <- 符号。
现在我们先使用以下语句定义一个通道:
ch := make(chan int, 1)
发送
将一个值发送到通道中。
ch <- 10 // 发送,把10发送到ch中
接收
从一个通道中接收值。
x := <- ch // 从ch中接收值并赋值给变量x <-ch // 从ch中接收值,忽略结果,直接丢弃
关闭
我们通过调用内置的close函数来关闭通道。
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。
通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
1. 对一个关闭的通道再发送值就会导致panic。 2. 对一个关闭的通道进行接收会一直获取值直到通道为空。 3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。 4. 关闭一个已经关闭的通道会导致panic。
无缓冲的通道 : 阻塞的通道
无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:
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/Q1mi/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同步化。
因此,无缓冲通道也被称为同步通道。
有缓冲的通道
解决上面问题的方法还有一种就是使用有缓冲区的通道。我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:
func main() { ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道 ch <- 10 fmt.Println("发送成功") }
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,
格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。
如何优雅的从通道循环取值
当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。
当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?
接收值判断通道是否关闭
func send(ch chan int){ for i:0;i<10;i++{ ch <- i } close(ch) // 可以被垃圾回收机制回收掉。 } func main(){ var ch1 = make(chan int,100) go send(ch1) // 1. 利用for 循环去通道ch1中接受值 for { ret, ok := <- ch1 // 使用value,ok 取值方式,当通道关闭的时候 ok=false if !ok{ break } } // 2。 利用 for range 循环去通道 ch1 中接受者 for ret:=range ch1 { fmt.Println(ret) } }
通道练习题
// 生产着 消费者模型 // 使用 goroutine 和channel 实现一个简易的生产消费者模型 // 生产着 : 产生随机数 math/rand // 消费者 : 计算每个随机数的每个位的数字的和 // 1个 生产者 20个消费者 var itemChan chan *item var resultChan chan *result var exitChan chan struct{} type item struct { id int num int64 } type result struct{ item *item sum int64 } // 生产者 func producer(ch chan *item){ // 1. 生成随机数 var id int64 for { id ++ number := rand.Int63() // int64 tmp := &item{ id : id, num : number, } // 2. 把随机数发送到通道中 ch <- tmp } } // 计算一个数字 每个位的和 func calc(num int64){ var sum int64 for num > 0 { sum = sum + num%10 // sum = 0+3 num = num/10 // num =12 } return sum } // 消费者 func consumer(ch chan *item, resultChan chan *result){ for tmp := range ch { // 结构体指针 *item ret := calc(tmp.num) // 构造result结构体 retObj := result{ item: tmp, sum : sum, } resultChan <- retObj } } func closeResult(ch chan struct{}){ for } // 打印结果 func printResult(resultChan chan *result){ for { select{ case ret:= <- resultChan: fmt.Printf("id: %v, num: %v, sum: %v ", ret.item.id , ret.item.num, ret.sum) case <- exitChan: break } } } func startWorker(n int,ch chan *item,resultChan chan *result ){ for i:=0; i<n ;i++{ go consumer(ch, resultChan) } } // 监听键盘输入,只要有输入就往 exitChan 发送值 func getInput(exitChan chan struct{}){ tmp:=[1]byte{} // 初始化 n,_ := os.Stdin.Read(tmp[:]) // 从标准输入获取值 if n > 0{ exitChan <- struct{}{} // 用户表示退出 } } func main(){ itemChan = make(chan *item, 100) resultChan = make(chan *result, 100) go producer(itemChan) startWorker(20, itemChan, resultChan) printResult(resultChan) } // 随机数 // 给 rand 加随机数种子,实现每一次执行都能产生真正的随机数 rand.Seed(time.Now().UnixNano()) // 添加随机数种子 ret1 := rand.Int63() // int64 正整数 fmt.Println(ret1) ret2 := rand.Intn(101) // [1,101] fmt.Println(ret2)
我们来看下面这个例子:
// 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的方式。
单向通道
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数
中只能发送或只能接收。
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) }
chan<- int 是一个只能发送的通道,可以发送但是不能接收;
<-chan int 是一个只能接收的通道,可以接收但是不能发送。
在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。
通道总结
关闭已经关闭的channel也会引发panic。
worker pool(goroutine池)
在工作中我们通常会使用可以指定启动的goroutine数量–worker pool模式,控制goroutine的数量,防止goroutine泄漏和暴涨。
一个简易的work pool示例代码如下:
func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("worker:%d start job:%d ", id, j) time.Sleep(time.Second) fmt.Printf("worker:%d end job:%d ", id, j) results <- j * 2 } } func main() { jobs := make(chan int, 100) results := make(chan int, 100) // 开启3个goroutine for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // 5个任务 for j := 1; j <= 5; j++ { jobs <- j } close(jobs) // 输出结果 for a := 1; a <= 5; a++ { <-results } }
select 多路复用
var ch1 = make(chan string, 100) var ch2 = make(chan string, 100) func f1(ch chan int){ for i:=0;i<math.MaxInt64 ;i++{ ch <- fmt.Sprintf("f1:%d",i) time.Sleep(time.Second) } } func f2(ch chan int){ for i:=0;i<math.MaxInt64;i++{ ch <- fmt.Sprintf("f2:%d",i) time.Sleep(time.Millusecond * 100) } } func main(){ go f1(ch1) // 往ch1这个通道中放f1 开头的字符串 go f2(ch2) // 往ch2这个通道中放f2 开头的字符串 for { select { case ret := <- ch1: fmt.Println(ret) case ret := <- ch2: fmt.Println(ret) default: fmt.Println("暂时取不到值") time.Sleep(time.Millisecond*500) } } }
在某些场景下我们需要同时从多个通道接收数据。
通时,如果没有数据可以接收将会发生阻塞。
你也许会写出如下代码使用遍历的方式来实现:
for{ // 尝试从ch1接收值 data, ok := <- ch1 // 尝试从ch2接收值 data, ok := <- ch2 … }
这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。
为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作
select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。
select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。
具体格式如下:
select{ case <-ch1: ... case data := <-ch2: ... case ch3<-data: ... default: 默认操作 }
举个小例子来演示下select的使用:
func main() { ch := make(chan int, 1) for i := 0; i < 10; i++ { select { case x := <-ch: fmt.Println(x) case ch <- i: } } } 结果: 0 2 4 6 8
使用select语句能提高代码的可读性。
可处理一个或多个channel的发送/接收操作。
如果多个case同时满足,select会随机选择一个。
对于没有case的select{}会一直等待,可用于阻塞main函数。
并发安全和锁
有时候在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变量的时候就会存在数据竞争,导致最后的结果与期待的不符。
互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个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同时等待一个锁时,唤醒的策略是随机的。
读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。
读写锁在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)) }
需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。
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是一个结构体,传递的时候要传递指针。
sync.Once
说在前面的话:这是一个进阶知识点。
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。
sync.Once只有一个Do方法,其签名如下:
func (o *Once) Do(f func()) {}
备注:如果要执行的函数f需要传递参数就需要搭配闭包来使用。
加载配置文件示例
延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。
因为预先初始化一个变量(比如在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] }
关闭channel示例
var wg sync.WaitGroup var once sync.Once func f1(ch1 chan<- int) { defer wg.Done() for i := 0; i < 100; i++ { ch1 <- i } close(ch1) } func f2(ch1 <-chan int, ch2 chan<- int) { defer wg.Done() for { x, ok := <-ch1 if !ok { break } ch2 <- x * x } once.Do(func() { close(ch2) }) // 确保某个操作只执行一次 } func main() { a := make(chan int, 100) b := make(chan int, 100) wg.Add(3) go f1(a) go f2(a, b) go f2(a, b) wg.Wait() for ret := range b { fmt.Println(ret) } }
sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。
这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
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) // 将int 类型转换成字符串类型 set(key, n) // 给map 设置键值对 fmt.Printf("k=:%v,v:=%v ", 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 ", key, value) wg.Done() }(i) } wg.Wait() }
原子操作
代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。
针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。
Go语言中原子操作由内置的标准库sync/atomic提供。
atomic包
方法 解释
func LoadInt32(addr *int32) (val int32) func LoadInt64(addr *int64) (val int64) func LoadUint32(addr *uint32) (val uint32) func LoadUint64(addr *uint64) (val uint64) func LoadUintptr(addr *uintptr) (val uintptr) func LoadPointer(addr *unsafe.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) 比较并交换操作
示例
我们填写一个示例来比较下互斥锁和原子操作的性能。
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包的函数/类型实现同步更好。
内容回顾
goroutine
1.并发和并行
2.goroutine 1.用户态的线程,
3.启动goroutine 1.使用关键字 go ,必须要一个函数 2.一个goroutine对应一个函数,这个函数就是要做的事
4.goroutine 原理 1. M:N 将M个goroutine 调度到N个操作系统的线程中执行。 N默认是操作系统的逻辑核心数
5. goroutine 和OS 线程的区别 1. goroutine 是用户态的线程,初始开销内存2KB, 可以随着需求自动扩充,最大能达到1GB 2. 一个OS线程是很重量级的,通常开销达2MB 3. 通过 runtime 运行时, runtime.GOMAXPROCS() 用来设置go并发的CPU核数。1.5版本 以后默认是全跑满 6. goroutine 的特点 1. main函数就是一个goroutine 2. 当 goroutine 对应的函数返回的时候goroutine 就结束了 3. main 函数所在的goroutine结束了, 由他启动的那些goroutine也就结束了 6. sync.WaitGroup 结构体类型 1. 是一个结构体, var wg sync.WaitGroup 2. 3个方法 wg.Add(n) 设置值 wg.Done() -1 wg.Wait() 等到所有
channel 管道
1. 是一种类型,一种引用的类型 2. channel的声明和初始化 1. var ch chan int 2. channel 声明之后要使用make函数初始化之后才能使用 ch=make(chan int,[cap]) 3. channel 的3个操作 1. 发送 ch <- 100 2. 接收 <- ch 丢弃,可以使用变量接收值, a:= <- ch 3. 关闭 close(ch) 关闭后的通道,还是可以取值的,取完之后返回的是类型零值 4. 无缓冲区channel和有缓冲区channel 1. 无缓冲区channel 又称为同步channel。必须有人接收才能发送,否则会一直阻塞 2. 有缓冲区的channel 超过容量就阻塞 5. 优雅地从通道取值 (能判断通道是否被关闭) 1. v, ok := <- ch 如果通道被关闭ok返回的是false 2. for v:= range ch{} 6. 单向通道 1. 只能接受( <-ch )或者只能发送( ch<- )的通道 2. 多用在函数传参的时候,限制某个通道再函数中只能做什么类型的操作 7. select 多路复用 1. 同一时刻可以对多个通道发送和接受操作 2. ch := make(chan int ,1) for i := 0;i<10; i++{ select { case ch<-i; case ret:= <-ch; fmt.Println(ret) } } 0 2 4 6 8
8.通道是线程安全的
并发控制与锁
1. 很多并发的场景下需要 goroutine之间做协同处理 2. 如果多个goroutine操作同一个全局变量的时候,就会存在数据竞争 3. 互斥锁 1. sync.Mutex, 它是一个结构体类型 2. 声明锁 1. var lock sync.Mutex 3. 操作 1. 加锁: lock.Lock() 2. 解说: lock.Unlock() 4. 读写锁 1. sync.RWMutex 适用于读多写少的场景,类似网站数据库的读写分离 2. 声明读写锁 1. var rwLock sync.RWMutex 3. 操作 1. 加读锁: rwLock.RLock() 2. 解读锁: rwLock.RUnlock() 3. 加写锁: rwLock.Lock() 4. 解写锁: rwLock.Unlock() 5. sync.Map 1. 内置的map不是并发安全的, 2. 并发场景下推荐使用 sync.Map 3. sync.Map 的使用 6. sync.Once 使用于 加载 init,文件 1. 闭包的应用
原子操作
代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。
针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作
更好。
Go语言中原子操作由内置的标准库sync/atomic提供。