A Tour of Golang (二)

前端之家收集整理的这篇文章主要介绍了A Tour of Golang (二)前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

是时候继续总结一波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]

退出的goroutine打印了栈信息然后退出而.

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种情况:

  • 调用runtime·gosched函数

    goroutine主动放弃cpu,该goroutine会被设置为runnable状态,然后放入一个全局等待队列中,而P将继续执行下一个goroutine。使用runtime·gosched函数是一个主动的行为,一般是在执行长任务时又想其它goroutine得到执行的机会时调用

  • 调用runtime·park函数

    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线程都能充分的使用。

猜你在找的Go相关文章