您的位置 首页 golang

Golang问题点(三) – Context的问题点

Go的标准库 context

在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。

那么如何控制goroutine优雅的退出呢?

1. 实例

1.0. 基础代码

package mainimport (    "fmt"    "sync"    "time")var wg sync.WaitGroupfunc DoSomething() {    for {        fmt.Println("董小贱")        time.Sleep(2 * time.Second)    }    wg.Done()}func main() {    wg.Add(1)  go DoSomething()  //   这个DoSomething() 没有停止条件,不会停止    wg.Wait()    fmt.Println("DoSomething over")}

1. 1全局变量方式

package mainimport (    "fmt"    "sync"    "time")var wg sync.WaitGroupvar status bool = truefunc DoSomething() {    for {        fmt.Println("董小贱")        time.Sleep(2 * time.Second)        if !status {            break        } else {            continue        }    }    wg.Done()}func main() {    wg.Add(1)    go DoSomething()    time.Sleep(10 * time.Second)    status = false  //通过修改全局变量的状态,给gorotine发送终止信号    wg.Wait()    fmt.Println("DoSomething over")}

1.2. select + channel 版本

package mainimport (    "fmt"    "sync"    "time")var wg sync.WaitGroupvar chann = make(chan bool)func DoSomething() {LoopTag:    for {        fmt.Println("董小贱")        time.Sleep(2 * time.Second)        select {        case <-chann: // 信号接收            break LoopTag // 退出整个lable        default:            continue        }    }    wg.Done()}func main() {    wg.Add(1)    go DoSomething()    time.Sleep(10 * time.Second)    chann <- false // 通过无缓存的channel来给goroutine发送终止信号    wg.Wait()    fmt.Println("DoSomething over")}

1.3.context版本

package mainimport (    "context"    "fmt"    "sync"    "time")var wg sync.WaitGroupfunc DoSomething(ctx context.Context) {LoopTag:    for {        fmt.Println("董小贱")        time.Sleep(2 * time.Second)        select {        case <-ctx.Done(): // 等待父级通知            break LoopTag        default:            continue        }    }    wg.Done()}func main() {    ctx, cancel := context.WithCancel(context.Background())    wg.Add(1)    go DoSomething(ctx)    time.Sleep(10 * time.Second)    cancel() //cancel()执行,通知子goroutine结束    wg.Wait()    fmt.Println("DoSomething over")}

当在DoSomething中再开启子goroutine时,将ctx传入到函数当中即可:

package mainimport (    "context"    "fmt"    "sync"    "time")var wg sync.WaitGroupfunc DoSomething2(ctx context.Context) {LoopTag:    for {        fmt.Println("董小小贱")        time.Sleep(2 * time.Second)        select {        case <-ctx.Done():            break LoopTag        default:            continue        }    }    wg.Done()}func DoSomething(ctx context.Context) {    go DoSomething2(ctx)LoopTag:    for {        fmt.Println("董小贱")        time.Sleep(2 * time.Second)        select {        case <-ctx.Done():            break LoopTag        default:            continue        }    }    wg.Done()}func main() {    ctx, cancel := context.WithCancel(context.Background())    wg.Add(1)    go DoSomething(ctx)    time.Sleep(10 * time.Second)    cancel()    wg.Wait()    fmt.Println("DoSomething over")}

那么context的作用就呼之欲出了:简单讲就是用来处理goroutine之间传递截止日期、取消信号和其他请求范围内的值

2. context相关


Go1.7 加入的新的标准库context 它定义了Context类型, 它跨API边界和进程之间传递截止日期、取消信号和其他请求范围内的值。

对服务器的传入请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传播上下文,可以选择将其替换为使用WithCancelWithDeadlineWithTimeoutWithValue创建的派生上下文。当一个上下文被取消时,从它派生的所有上下文也被取消。

WithCancelWithDeadlineWithTimeout函数接收一个Context并返回一个派生子Context和一个CancelFunc。调用CancelFunc取消子进程及其子进程,删除父进程对子进程的引用,并停止所有相关的计时器。如果没有调用CancelFunc,则会泄漏子节点及其子节点,直到父节点被取消或计时器触发。

不要将Context存储在结构类型中,应该显示地将上下文传递给需要它的每个函数,Context应该是第一个参数,通常命名为ctx。


2.1 context包相关

2.1.1 context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下:
type Context interface {    Deadline() (deadline time.Time, ok bool)    Done() <-chan struct{}    Err() error    Value(key interface{}) interface{}}// Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);//Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel;//Err方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值;        //如果当前Context被取消就会返回Canceled错误;        //如果当前Context超时就会返回DeadlineExceeded错误;//Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;
2.1.2 CancelFunc
type CancelFunc func()// CancelFunc告诉一个goroutine放弃它的工作。CancelFunc不会等待工作停止。在第一次调用之后,对CancelFunc的后续调用什么也不做。
2.1.3 Background()
func Background() Context// Background返回一个非nil的空上下文。它从未被取消,没有值,也没有期限。它通常由主函数、初始化和测试使用,并作为传入请求的根context(最顶层的context,其他的都是它的派生子context)
2.1.4 TODO()
func TODO() Context//TODO返回一个非nil的空context。代码应该使用context。TODO当不清楚要使用哪个context或者它还不可用时(因为周围的函数还没有被扩展到接受context参数)。TODO由静态分析工具识别,该工具确定context是否在程序中正确传播//这个就是知道要用context,但不清楚用哪个,埋点用。
backgroundtodo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

2.2 With系列函数

2.2.1 WithCancel
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)//WithCancel返回带有新Done通道的父进程的一个副本。当返回的cancel函数被调用时,或者当父context的Done通道被关闭时,返回context的Done通道将被关闭,以最先发生的情况为准。// 取消此context释放与之关联的资源,因此代码应该在此context中运行的操作完成后立即调用cancel。

具体用法可以看上边的例子。

2.2.2 WithDeadline
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)// deadline 是绝对时间,比如:d := time.Now().Add(50 * time.Millisecond)//WithDeadline返回父context的一个副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。返回的上下文的Done通道在deadline过期时关闭,在调用返回的cancel函数时关闭,或者在关闭父上下文的Done通道时关闭,以最先发生的情况为准。// 取消此上下文释放与之关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

一个例子:

func main() {    d := time.Now().Add(50 * time.Millisecond)    ctx, cancel := context.WithDeadline(context.Background(), d)    defer cancel()    select {    case <-time.After(1 * time.Second):        fmt.Println("overslept")    case <-ctx.Done():        fmt.Println(ctx.Err())    }}// 这里会先执行ctx.Done() 然后打印异常
2.2.1 WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)// 跟WithDeadline的区别在于,WithTimeout是用的相对时间。
2.2.1 WithValue
func WithValue(parent Context, key, val interface{}) Context//WithValue返回父节点的一个副本,其中与键关联的值为val。//只将上下文值用于传输流程和api的请求范围的数据,而不用于将可选参数传递给函数。// 提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用context的包之间的冲突。WithValue的用户应该定义自己的键类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。

一个例子:

package mainimport (    "context"    "fmt")type favContextKey stringfunc main() {    f := func(ctx context.Context, k favContextKey) {        if v := ctx.Value(k); v != nil {            fmt.Println("found value:", v)            return        }        fmt.Println("key not found:", k)    }    k := favContextKey("language")    ctx := context.WithValue(context.Background(), k, "Go")    f(ctx, k)    f(ctx, favContextKey("color"))}

2.3 context的注意事项:

- 推荐以参数的方式显示传递Context- 以Context作为参数的函数方法,应该把Context作为第一个参数,显示传递。​- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()- Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数- Context是线程安全的,可以放心的在多个goroutine中传递

文章来源:智云一二三科技

文章标题:Golang问题点(三) – Context的问题点

文章地址:https://www.zhihuclub.com/707.shtml

关于作者: 智云科技

热门文章

网站地图