是时候继续总结一波golang使用心得了!码的代码越多了解的go就越多,go处理问题的思路确实不一样
9. defer panic recover
defer
接上次的问题继续讨论,先来看下golang blog上怎么说defer
A defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform varIoUs clean-up actions.
个人感觉像是像 C++放在 destructor 中做的事一样,但是仔细考量下来还是不一样
1. defer语句不会进行求值,在声明defer语句的时进行计算,而不是call defer的时候.
2. 多个defer是 LIFO 的,因为就是说,最后声明的defer会最先call
3. defer可以给函数的return赋值或者进行修改,也就是说你的函数返回值可能会跟你的预期不符,如果你在defer中改变了返回的对象.
func c() (i int) {
defer func() { i++ }()
return 1
} \\函数最终返回的是2而不是1
panic
panic比较好理解,一旦触发了panic,这个内置的函数将会导致整个调用栈退出.
触发panic可能是由于 runtime error,当然也可以手动的调用
recover
recover用于从panic流程中重新获得控制权.因此recover只能被定义在defer中才会有效果.主动调用recover只会返回nil而且其他什么也不会发生…
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f",r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v",i))
}
defer fmt.Println("Defer in g",i)
fmt.Println("Printing in g",i)
g(i + 1)
}
正常的情况下,输出应该可以预见
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.
如果去掉recover 可以看一下结果
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4
panic PC=0x2a9cd8
[stack trace omitted]
10.接口
实现
go接口编程十分优雅,采用的是叫做 非嵌入式接口的模型:
传统的面向对象模型中,接口被设计的需要明确的继承关系,这种关系在声明的时候就必须确定,例如 C++ 和 JAVA. 尽管JAVA采用了implement这种方式表述接口,但接口仍然没有脱离C++中的模型:继承与派生之间的强依赖关系.
go认为,接口之间的这种依赖应该是单向的.接口的使用者不关心接口的实现方式.接口的实现方也不应该提前知道使用方所有的需求的接口.也就是说接口应该是定义了一种规范,双方都遵循的规范,但是对于双方来讲,并不应该存在依赖关系.
go中,一旦有类实现的某个接口所要求的所有的函数,那么就是实现的这个接口.因此go中不应该存在继承树这样的东西:你只需要知道你要提供的是哪些功能.
接口赋值
不过,go同样有一些特殊的规定:
type Integer int
func (a Integer) less (b Integer) bool {
}
func (a *Integer) bigger (b *Integer) bool {
}
type IntInterface interface{
less (a Integer) bool
bigger (a Integer) bool
}
这种情况下的赋值一个接口对象,应该使用
var a Integer = 1
var b IntInterface = &a
在go中会根据less 自动的生成 对于 *Integer 的less方法,而无法从指针类型的方法推导出非指针的方法.
接口查询 & 类型查询
...
if value,ok := object.(type_name); ok{
}
...
也可以更直接的使用switch避免进一步转换
...
switch v := object.(type){
case int :
case string:
...
11.goroutine 模型&调度
goroutine 是go语言并发的实现方式,goroutine其实是一种对于Coroutine的实现。go语言通过简单易用的goroutine使得并发程序非常容易写出来。
看到的知乎上一个系统的回答,拿来引用并且改动了一部分:
http://www.zhihu.com/question/20862617/answer/27964865
用户空间线程和内核空间线程之间的映射关系有:N:1,1:1和M:N
N:1是说,多个(N)用户线程始终在一个内核线程上跑,context上下文切换确实很快,但是无法真正的利用多核。
1:1是说,一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文switch很慢。
M:N是说, 多个goroutine在多个内核线程上跑,这个看似可以集齐上面两者的优势,但是无疑增加了调度的难度。
一般来说,如果没有显式的让出cpu,就会一直执行当前协程。关于goroutine何时调度,一般会有3种情况:
-
goroutine主动放弃cpu,该goroutine会被设置为runnable状态,然后放入一个全局等待队列中,而P将继续执行下一个goroutine。使用runtime·gosched函数是一个主动的行为,一般是在执行长任务时又想其它goroutine得到执行的机会时调用。
-
goroutine进入waitting状态,除非对其调用runtime·ready函数,否则该goroutine将永远不会得到执行。而P将继续执行下一个goroutine。使用runtime·park函数一般是在某个条件如果得不到满足就不能继续运行下去时调用,当条件满足后需要使用runtime·ready以唤醒它(这里唤醒之后是否会加入全局等待队列还有待研究)。像channel操作,定时器中,网络poll等都有可能park goroutine。
慢系统调用。
这样的系统调用会阻塞等待,为了使该P上挂着的其它G也能得到执行的机会,需要将这些goroutine转到另一个OS线程上去。具体的做法是:首先将该P设置为syscall状态,然后该线程进入系统调用阻塞等待。之前提到过的sysmom线程会定期扫描所有的P,发现一个P处于了syscall的状态,就将M和P分离(实际上只有当 Syscall 执行时间超出某个阈值时,才会将 M 与 P 分离)。RUNTIME会再分配一个M和这个P绑定,从而继续执行队列中的其它G。而当之前阻塞的M从系统调用中返回后,会将该goroutine放入全局等待队列中,自己则sleep去。
Go的调度器内部有三个重要的结构:M,P,S
M:代表真正的内核OS线程,和POSIX里的thread差不多,真正干活的人
G:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
P:代表调度的上下文,可以把它看做一个局部的调度器,使go代码在一个线程上跑,它是实现从N:1到N:M映射的关键。(这就是为甚么 GOMAXPROCES 可以设置的比系统大的原因)
图中看,有2个物理线程M,每一个M都拥有一个context(P),每一个也都有一个正在运行的goroutine。
P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。
图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),
Go语言里,启动一个goroutine很容易:go func() 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出一个goroutine执行。
为何要维护多个上下文P?因为当一个OS线程被阻塞时,P可以转而投奔另一个OS线程!
图中看到,当一个OS线程M0陷入阻塞时,P转而在OS线程M1上运行。调度器保证有足够的线程来运行所以的context P。
图中的M1可能是被创建,或者从线程缓存中取出。
当MO返回时,它必须尝试取得一个context P来运行goroutine
1. 一般情况下,它会从其他的OS线程那里steal偷一个context过来 2. 如果没有偷到的话,它就把goroutine放在一个global runqueue里,然后自己就去睡大觉了(放入线程缓存里)。
Contexts们也会周期性的检查global runqueue,否则global runqueue上的goroutine永远无法执行。
另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了一个上下文P闲着没事儿干而系统却任然忙碌。但是如果global runqueue没有任务G了,那么P就不得不从其他的上下文P那里拿一些G来执行。一般来说,如果上下文P从其他的上下文P那里要偷一个任务的话,一般就‘偷’run queue的一半,这就确保了每个OS线程都能充分的使用。