接上一篇
Go 语言通过内置的错误接口处理错误。
error类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
Go语言提供了一种机制,在不知道具体类型的情况下,可以用反射来更新变量值,查看变量类型
Typeof返回接口中保存的值得类型,Typeof(nil)会返回nil
ValueOf返回一个初始化为interface接口保管的具体值得Value,ValueOf(nil)返回Value零值
2.1通过反射获取值
bookauthor := "www.w3cschool.cn"
bookdetail := make(map[string]string)
bookdetail["Go语言教程"]="www.w3cschool.cn"
fmt.Println(reflect.ValueOf(bookauthor))//www.w3cschool.cn
fmt.Println(reflect.ValueOf(bookdetail))//map[Go语言教程:www.w3cschool.cn]
2.2通过反射设置值
//通过这个函数设置值
func reflectsetvalue1(x interface{}){
value:=reflect.ValueOf(x)
if value.Kind() == reflect.String{
value.SetString("欢迎来到W3Cschool")
}
}
//通过这个函数设置值
func reflectsetvalue2(x interface{}){
value:=reflect.ValueOf(x)
// 反射中使用Elem()方法获取指针所指向的值
if value.Elem().Kind() == reflect.String{
value.Elem().SetString("欢迎来到W3Cschool")
}
}
func main() {
address := "www.w3cschool.cn"
// reflectsetvalue1(address)
// 反射修改值必须通过传递变量地址来修改。若函数传递的参数是值拷贝,则会发生下述错误。
// panic: reflect: reflect.Value.SetString using unaddressable value
reflectsetvalue2(&address)
fmt.Println(address)//欢迎来到W3Cschool
}
使用建议
1、大量使用反射的代码通常会变得难以理解
2、反射的性能低下,基于反射的代码会比正常的代码运行速度慢一到两个数量级
并发与并行
并发:同一时间段内执行多个任务
并行:同一时刻执行多个任务
Go语言中的并发程序主要是通过基于CSP(communicating sequential processes)的goroutine和channel来实现,当然也支持使用传统的多线程共享内存的并发方式
使用goroutine:在函数或者方法前面加上go关键字就可以创建一个goroutine,从而让该函数或方法在新goroutine中执行。
匿名函数也支持使用go关键字来创建goroutine去执行。
一个goroutine必定对应一个函数或者方法,可以创建多个goroutine去执行相同的函数或者方法。
func hello() {
fmt.Println("hello")
}
func main() {
go hello()//创建一个goroutine,让hello函数在新的routine中执行
fmt.Println("欢迎来到编程狮")
}
//此时的输出结果为
欢迎来到编程狮
hello
这个结果是因为在程序中创建 goroutine (可以理解为go任务)执行函数需要一定的开销,而与此同时 main 函数所在的 goroutine 是继续执行的。
补充:sync.WaitGroup
在上述代码中使用time.sleep的方法是不准确的。
Go语言中的sync包为我们提供了一些常用的并发原语(原语一旦开始执行,就要连续执行完,不允许中断 。)
介绍一下sync包中的WaitGroup。当你并不关心并发操作的结果或者有其它方式收集并发操作的结果时,WaitGroup是实现等待一组并发操作完成的好方法。
defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行
var wg sync.WaitGroup
func hello() {
fmt.Println("hello")
defer wg.Done()//把计算器-1(将 WaitGroup 的计数值减一,可以理解为完成一个子任务)
}
func main() {
wg.Add(1)//把计数器+1(设置 WaitGroup 的计数值,可以理解为子任务的数量)
go hello()
fmt.Println("欢迎来到编程狮")
wg.Wait()//阻塞代码的运行,直到计算器为0(阻塞调用者,直到 WaitGroup 的计数值为0,即所有子任务都完成)
}
//输出结果如下
欢迎来到编程狮
hello
var wg sync.WaitGroup
func hello(i int) {
fmt.Printf("hello,欢迎来到编程狮%v\n", i)
defer wg.Done()//goroutine结束计数器-1
}
func main() {
for i := 0; i < 10; i++ {
go hello(i)
wg.Add(1)//启动一个goroutine计数器+1
}
wg.Wait()//等待所有的goroutine执行结束
}
输出顺序并不一致,这是因为10个goroutine都是并发执行的,而goroutine的调度是随机的。
动态栈
操作系统的线程一般都有固定的栈内存(通常为2MB),而 Go 语言中的 goroutine 非常轻量级,一个 goroutine 的初始栈空间很小(一般为2KB),所以在 Go 语言中一次创建数万个 goroutine 也是可能的。并且 goroutine 的栈不是固定的,可以根据需要动态地增大或缩小, Go 的 runtime 会自动为 goroutine 分配合适的栈空间。
goroutine调度
在经过数个版本迭代之后,目前Go语言的调度器采用的是GPM调度模型
G: 表示goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;另外G对象是可以重用的。
P: 表示逻辑processor,P的数量决定了系统内最大可并行的G的数量(前提:系统的物理cpu核数>=P的数量);P的最大作用还是其拥有的各种G对象队列、链表、一些cache和状态。
M: M代表着真正的执行计算资源。在绑定有效的p后,进入schedule循环;而schedule循环的机制大致是从各种队列、p的本地队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到m,如此反复。M并不保留G状态,这是G可以跨M调度的基础。
GOMAXPROCS
Go运行时,调度器使用GOMAXPROCS的参数来决定需要使用多少个OS线程来同时执行Go代码。默认值是当前计算机的CPU核心数。例如在一个8核处理器的电脑上,GOMAXPROCS默认值为8。Go语言中可以使用runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU核心数。
单纯地将函数并发执行是没有意义的,函数与函数间需要交换数据才能体现并发执行函数的意义
虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,很多并发模型中必须使用互斥锁对内存进行加锁,这种做法势必造成性能问题
Go语言采用的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存,而不是通过共享内存而实现通信
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
2.1channel使用
channel类型
声明通道类型变量:var 变量名 chan 元素类型
其中chan是关键字,元素类型指通道中传递的元素的类型
channel零值
未经初始化的通道默认值为nil
初始化channel
make函数初始化之后才能使用,格式:make(chan 元素类型,[缓冲大小])
b:=make(chan int,10)//声明一个缓冲大小为10的通道
channel操作
发送,接收,关闭三种操作,而发送和接收操作均用<-符号
2.2无缓冲的通道
无缓冲的通道又称为阻塞的通道
func main() {
a := make(chan int)
a <- 10
fmt.Println("发送成功")
}
上面这段代码能够通过编译,但是执行时会报错
用golang!