Goroutine使用准则

前言

Gouroutine是Go开发中最常用的功能之一,开启一个Goroutine很简单,但是想要控制一个Goroutine的生命周期却需要一些技巧

最常见的是goroutine泄露问题

1
2
3
4
5
6
7
func leak(){
ch:=make(chan int)
go func(){
val:=<-ch
fmt.PrintIn("received a value:",val)
}()
}

函数退出后goroutine并不会退出,而且也不会接收到数据,处于一直被堵塞的状态,并且会占用资源,这就是常见的goroutine泄露(你以为它结束了,其实它没有)

准则

goroutine最常用的准则

不要开启一个goroutine如果你不知道它什么时候结束

任何时候开启一个goroutine都要问自己两个问题

  • 它什么时候结束
  • 如何让它结束

方法

设置超时控制

对于一些goroutine应该还要设置超时控制,使用context

goroutine一个超时时间,这样goroutine的结束就在掌握之中

1
2
3
4
5
6
7
8
func goroutine_1() {
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
defer cancel()

go func(ctx context.Context) {
xxx()
}(ctx)
}

设置recover兜底保护

对于一个我们不知道什么时候会启用的goroutine称为野生goroutine(如调用指定接口其业务逻辑会启动一个goroutine

Gin框架中,在我的业务逻辑接口中,即使发生了Panic行为(如数组越界,使用未初始化的map等)也不会使整个程序崩溃,因为在Gin为每一个调用者都自动启动revover( )进行兜底保护

但是如果在业务逻辑的接口中又使用了goroutine,并且在goroutine中发生了Panic行为,那么就会导致整个程序的崩溃

所以我们对于野生的goroutine要使用recover( )进行兜底保护

1
2
3
4
5
6
7
8
9
10
func goroutine_2() {
go func() {
defer func() {
if err := recover(); err != nil {
logger.Errorf("happen panic!")
}
}()
xxx()
}()
}

使用chan来监听和控制goroutine的生命周期

比如在main函数中同时监听两个端口的正确姿势

  • 同时监听,平滑退出
  • 能在main中管理goroutine的生命周期并知道它何时退出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
done := make(chan error, 2)
stop := make(chan struct{})
go func() {
done <- rpc.GatewayRPC(stop)
}()
go func() {
done <- api.ServiceAPI(stop)
}()

var stopped bool
for i := 0; i < cap(done); i++ {
if err := <-done; err != nil {
fmt.Println("err: ", err)
}
if !stopped {
stopped = true
close(stop)
}
}
}

使用空结构体传递信号(空结构体不占用内存,用来作为通道的信号传递最好)

一旦某个goroutine出现错误就会发送错误信息给done

done接收到信息后结汇关闭stop,每个有stop通道的goroutine收到信号后就会优雅关闭服务(Shutdown函数)

1
2
3
4
5
6
7
8
9
10
11
12
func ServiceAPI(stop <-chan struct{}) error {
r := router.AgentRouter()
server := &http.Server{
Addr: ":8080",
Handler: r,
}
go func() {
<-stop
server.Shutdown(context.TODO())
}()
return server.ListenAndServe()
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!