有时我们在main中启动goroutine处理任务,但我们并不想main函数退出,在C/C++的编程中我们常使用死循环+sleep保持main运行,不会走到函数结束,Go也可以使用该方式实现main的不退出。

  1. package main
  2. // main.go
  3. import (
  4. "fmt"
  5. "time"
  6. )
  7. func Handle(msg string) {
  8. fmt.Println(msg)
  9. }
  10. func main() {
  11. for i:=0; i<10; i++ {
  12. go Handle(fmt.Sprintf("handler %d", i))
  13. }
  14. for {
  15. time.Sleep(time.Second * 1)
  16. }
  17. }

在Go中也可以使用select{}让函数阻塞,而不需要用for{}这样的死循环空跑。select语句不包括case和default,那么将永久阻塞。

package main

// main.go

import (
    "fmt"
    "time"
)

func Handle(msg string) {
    fmt.Println(msg)
}

func main() {
    for i:=0; i<10; i++ {
        go Handle(fmt.Sprintf("handler %d", i))
    }

    select {

    }
}

select在main函数中做永久阻塞是一个常用的手法,当然select也有其他重要的应用场景,比如下面介绍的超时控制和快速检错,就是select在Go编程中使用得比较多的手法了。

超时控制

select一个重要的场景是做超时等待,比如我们启动一个协程去处理一个请求或任务时,很多时候我们当前协程是不知道自己的子协程的任务进度的,如果子协程陷入死循环或者阻塞时,如果当前协程没有合适的处理方式,那有可能自己也会被阻塞住,影响后面的执行逻辑;也有可能子协程跑满了CPU,导致整个系统陷入不可用。因此,我们希望对自己的协程做超时控制,当子协程/函数执行的时间超出我们的预期时,我们可以判断为是执行异常了,需要释放资源,恢复正常逻辑。

超时控制是一个可靠系统的基本要求,当局部模块异常时,我们可以快速地释放异常模块占有的资源(锁,CPU调度,打开的文件,网络连接等),并从异常中恢复。Go中可以使用select+channel的方式优雅地实现这个超时控制逻辑。

基于上面的例子,为了支持监控Handle的执行时间,我对Handle再包装一层为handleMgr,引入channel+select的方式,对每个Handle协程进行超时控制。

包装了handle函数的handleMgr的实现如下,核心思想就是在handleMgr再起一个协程,这个协程就是真实要执行的handle,而外面启动的协程handleMgr用于管理这个handle。

select结构的time.After(Timeout)控制了超时时间,一旦case result := <-ch: 这个分支没有在限定时间内收到消息,那就会触发走入time.After(Timeout)分支,触发超时处理机制,幷结束这个handleMgr协程,回收当前协程以及其子协程资源。

type HandleFunc func(msg string, v int) (result string)

func handleMgr(f HandleFunc, msg string, v int) {

    ch := make(chan string)
    Timeout := time.Second * 5
    go func() {
        result := f(msg, v)
        ch <- result
    } ()

    select {
    case <-time.After(Timeout):
        fmt.Printf("goroutine %v handle timeout: expect within %s\n", f, Timeout)
        TimeoutHandler()
        return
    case result := <-ch:
        fmt.Printf("goroutine %v handle OK, result %s\n", f, result)
        return
    }
}

完整的例子如下,main函数中开了10个handleMgr协程,传入的是Handle函数指针,需要执行的是Handle的逻辑。幷在handleMgr中设置了5秒的超时,一个Handle函数我们期望最多执行5秒,超出该执行时间就是异常,需要赶紧回收其资源。

package main

// main.go

import (
    "fmt"
    "time"
    "runtime"
)

type HandleFunc func(msg string, v int) (result string)

func Handle(msg string, t int) string {
    time.Sleep(time.Second * time.Duration(t))
    fmt.Println(msg)

    return msg + " Handle Done"
}

func TimeoutHandler() {
    fmt.Println("TimeoutHandler handling")
}

func handleMgr(f HandleFunc, msg string, v int) {

    ch := make(chan string)
    Timeout := time.Second * 5
    go func() {
        result := f(msg, v)
        ch <- result
    } ()

    select {
    case <-time.After(Timeout):
        fmt.Printf("goroutine %v handle timeout: expect within %s\n", f, Timeout)
        TimeoutHandler()
        return
    case result := <-ch:
        fmt.Printf("goroutine %v handle OK, result %s\n", f, result)
        return
    }
}

func main() {
    for i:=0; i<10; i++ {
        go handleMgr(Handle, fmt.Sprintf("handler %d", i), i)
    }

    for {
        time.Sleep(time.Second * 1)
    }
}

输出如下,handle0至4都是在5秒超时时间内完成了执行,而handle5至9的handler都超时了,走入handleMgr的select超时分支,进行了超时处理。

junshideMacBook-Pro:gogogo junshili$ go run main.go 
handler 0
goroutine 0x10a3830 handle OK, result handler 0 Handle Done
handler 1
goroutine 0x10a3830 handle OK, result handler 1 Handle Done
handler 2
goroutine 0x10a3830 handle OK, result handler 2 Handle Done
handler 3
goroutine 0x10a3830 handle OK, result handler 3 Handle Done
handler 4
goroutine 0x10a3830 handle OK, result handler 4 Handle Done
handler 5
goroutine 0x10a3830 handle timeout: expect within 5s
TimeoutHandler handling
goroutine 0x10a3830 handle timeout: expect within 5s
TimeoutHandler handling
goroutine 0x10a3830 handle timeout: expect within 5s
TimeoutHandler handling
goroutine 0x10a3830 handle timeout: expect within 5s
TimeoutHandler handling
goroutine 0x10a3830 handle timeout: expect within 5s
TimeoutHandler handling
handler 6
handler 7
handler 8
handler 9

快速检错

使用channel来对不同协程间传递错误是常用的方法,比如我们上面的例子中,如果handleMgr的子协程发生了错误/panic时,我们可以使用channel来通知handleMgr,让handleMgr进行相应的错误处理。总体思路就是子协程使用channel传递错误,父协程在函数中使用select来监听这个chan即可。我们只需简单在上面的超时控制版本扩展一下就可以实现快速检错的功能。

package main

// main.go

import (
    "fmt"
    "time"
    "errors"
)

type HandleFunc func(msg string, v int) (result string, err error)

func Handle(msg string, t int) (string, error) {
    time.Sleep(time.Second * time.Duration(t))
    fmt.Println(msg)

    var err error
    if t == 3 {
        err = errors.New("handle 3 err")
    }

    return msg + " Handle Done", err
}

func TimeoutHandler() {
    fmt.Println("TimeoutHandler handling")
}

func handleMgr(f HandleFunc, msg string, v int) {

    ch := make(chan string)
    errChan := make(chan error)
    Timeout := time.Second * 5
    go func() {
        result, err := f(msg, v)
        if err != nil {
            errChan <- err
        } else {
            ch <- result
        }
    } ()

    select {
    case <-time.After(Timeout):
        fmt.Printf("goroutine %v handle timeout: expect within %s\n", f, Timeout)
        TimeoutHandler()
        return
    case result := <-ch:
        fmt.Printf("goroutine %v handle OK, result %s\n", f, result)
        return
    case err := <-errChan:
        fmt.Printf("goroutine %v handle error, err %s\n", f, err)
        return
    }
}

func main() {
    for i:=0; i<10; i++ {
        go handleMgr(Handle, fmt.Sprintf("handler %d", i), i)
    }

    for {
        time.Sleep(time.Second * 1)
    }
}

输出如下,handle3 跑进了case err := <-errChanselect分支,并触发了错误处理流程。

junshideMacBook-Pro:gogogo junshili$ go run main.go 
handler 0
goroutine 0x10a35a0 handle OK, result handler 0 Handle Done
handler 1
goroutine 0x10a35a0 handle OK, result handler 1 Handle Done
handler 2
goroutine 0x10a35a0 handle OK, result handler 2 Handle Done
handler 3
goroutine 0x10a35a0 handle error, err handle 3 err
handler 4
goroutine 0x10a35a0 handle OK, result handler 4 Handle Done
goroutine 0x10a35a0 handle timeout: expect within 5s
TimeoutHandler handling
handler 5
goroutine 0x10a35a0 handle timeout: expect within 5s
TimeoutHandler handling
goroutine 0x10a35a0 handle timeout: expect within 5s
goroutine 0x10a35a0 handle timeout: expect within 5s
TimeoutHandler handling
goroutine 0x10a35a0 handle timeout: expect within 5s
TimeoutHandler handling
TimeoutHandler handling
handler 6
handler 7
handler 8
handler 9