从使用角度浅谈 Go context

·  阅读 1212
从使用角度浅谈 Go context

本文正在参加「金石计划 . 瓜分6万现金大奖」

从使用角度浅谈 Go context

〇、前言

这是《让我们一起Golang》专栏的第47篇文章,本文从使用角度浅谈Go的context包,不过由于笔者水平和工作经验限制,可能文章存在许多不足或错误,烦请指出斧正!

本专栏的其他文章:

一、context的作用是什么?

context译为“上下文”,主要在协程Goroutine之间传递上下文信息,包括取消信号、超时时间、截止时间、键值对等等。

比如我们用Go语言搭建一个HTTP server,如果有一个请求发送到server,它就会启动一个或多个Goroutine去工作,如果响应请求时调用的某个服务响应速度很慢,就会导致请求这个服务的Goroutine越来越多,导致内存占用暴涨,Go调度器和垃圾回收器压力很大,就会导致发送到server的请求得不到响应。

而context包的作用就是解决这个问题。

在Go里面,没有context包前,我们一般使用channel和select来控制协程的关闭,但是当多个协程之间互相关联,有共享的数据时,使用channel和select就会比较麻烦,此时我们就需要用到context包。

二、context的使用:传递共享数据

package main
​
import (
    "context"
    "fmt"
)
​
func main() {
    //上下文默认值,所有其他的上下文都从他衍生。通常用于main函数、初始化、测试或者顶级上下文
    ctx := context.Background()
    s, ok := ctx.Value("name").(string)
    if !ok {
        fmt.Println("nil")
    } else {
        fmt.Println(s)
    }
    //基于某个 context 创建并存储对应的上下文信息。
    ctx = context.WithValue(ctx, "name", "ReganYue")
    s, _ = ctx.Value("name").(string)
    fmt.Println(s)
}
​
复制代码

context.Background()是上下文默认值,所有其他的上下文都从他衍生。通常用于main函数、初始化、测试或者顶级上下文。context.WithValue()是基于某个 context 创建并存储对应的上下文信息。

下面是运行结果:

image.png

第一次取上下文中的“name”时,因为ctx是一个空的context,因此取不出来,ok为false,因此我们输出的是nil,后面使用context.WithValue()创建并存储了上下文信息,因此第二次取时,能够取到“name”中的数据ReganYue

二、context的使用:定时取消

当某个服务因为业务负载过重、网络延迟高的情况导致请求阻塞时,需要用到context包中的WithTimeout(基于父级 context,创建一个具有超时时间(Timeout)的新 context)。

package main
​
import (
    "context"
    "fmt"
    "time"
)
​
func main() {
    timeout, cancelFunc := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancelFunc()
​
    select {
    case <-time.After(2 * time.Second):
        fmt.Println("Hello")
    case <-timeout.Done():
        fmt.Println("finish finish")
        
    }
​
}
复制代码

如果是这样的话,会输出:

finish finish
​
​
复制代码

因为已经context.WithTimeout设置的定时是1秒钟,1秒钟过后,timeout就会Done了,所以输出finish finish,然后再调用cancelFunc()。如果我们将context.WithTimeout设置的定时大于2秒钟,比如5秒钟,就会输出:

Hello
​
​
复制代码

三、context的使用:避免Goroutine泄露

上述使用案例中,若不使用context,Goroutine仍然会执行完毕,但是某些场景下,若不用context取消,会导致goroutine泄露。

看下面这个例子:

package main
​
import (
    "fmt"
    "time"
)
​
func gen() <-chan int {
    ch := make(chan int)
    go func() {
        var n int
        for {
            ch <- n
            n++
            time.Sleep(1 * time.Second)
        }
    }()
    return ch
}
​
func main() {
    for n := range gen() {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
​
    fmt.Println("....")
}
复制代码

在该例子中,当取整数5后,我们直接break,这时候往管道ch发送数字的协程Goroutine就会被阻塞,我们常称之为Goroutine泄露。

如何用context解决这个问题呢?我们看看下面:

package main
​
import (
    "context"
    "fmt"
    "time"
)
​
func gen(ctx context.Context) <-chan int {
    ch := make(chan int)
    go func() {
        var n int
        for {
            select {
            case ch <- n:
                n++
                time.Sleep(1 * time.Second)
            case <-ctx.Done():
                return
            }
​
        }
    }()
    return ch
}
​
func main() {
    ctx, cancelFunc := context.WithCancel(context.Background())
    defer cancelFunc()
​
    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            cancelFunc()
            break
        }
    }
​
    fmt.Println("....")
}
​
复制代码

在主协程中调用break前,我们调用cancelFunc,让执行gen函数的协程执行return,让GC回收资源。

参考文献:

深度解密Go语言之context - 知乎 zhuanlan.zhihu.com/p/68792989

Context should go away for Go 2 — faiface blog faiface.github.io/post/contex…

Go程序员面试笔试宝典 - 机械工业出版社

收藏成功!
已添加到「」, 点击更改