golang并发编程实战(《Go 语言并发之道》读书笔记(六))
今天这篇笔记我们来学习一下context包
context包的一个应用场景是可以通过它控制goroutine的取消 ,超时等 。
我们先来看一个取消的例子context.WithCancel
func doSomething(ctx context.Context) { ctx, cancelctx := context.WithCancel(ctx) printCh := make(chan int) go doAnother(ctx, printCh) for i := 0; i < 3; i++ { printCh <- i } cancelctx() fmt.Println("do something finished") } func doAnother(ctx context.Context, printCh chan int) { for { select { case <-ctx.Done(): if err := ctx.Err(); err != nil { fmt.Printf("do Another with error, %s \n", err) } fmt.Println("do Another finished") return case data := <-printCh: fmt.Printf("receive %d \n", data) } } }我们在doSomething方法中加了一个可以取消的Context , 然后定义了一个channel, 往channel里面放入3个数 ,另外启动一个goroutine doAnother 来接收它, 在完成放入数据后我们调用了取消方法 , 通知doAnother 结束掉 。 这样程序就达到一个goroutine通知另外一个goroutine的效果 , 程序运行结果如下
receive 0 receive 1 receive 2 do Another with error, context canceled do Another finished do something finished通过打印的输出我们可以看到doAnother是通过canneled来结束的 。
如果我们再加一个goroutine ,也能够通过context来取消 。 这里比较简单不贴代码了 。context.WithTimeout
如果只是单纯的通知另外一个goroutine ,直接通过close channel也可以做到了 , Context还可以通过WithTimeout设定超时时间来限定程序运行多久 ,我们简单改造一下上面的代码
func doSomething(ctx context.Context) { ctx, cancelctx := context.WithTimeout(ctx, 1500*time.Millisecond) defer cancelctx() printCh := make(chan int) go doAnother(ctx, printCh) outer: for i := 0; i < 3; i++ { select { case printCh <- i: time.Sleep(1 * time.Second) case <-ctx.Done(): break outer } } cancelctx() fmt.Println("do something finished")我们把doSomething改造了一下 , 通过context.WithTimeout(ctx, 1500*time.Millisecond)将context 改成运行1.5秒就退出, doAnother 保持不变 , 然后发送的时候 ,发送一个就sleep 1秒 。 运行效果如下
receive 0 receive 1 do Another with error, context deadline exceeded do Another finished do something finished我们可以看到只发送了两个数,就退出了 ,退出的原因是exceeded. 这样就达到了控制goroutine运行时间的效果 。
通过context.WithDeadline 也可以达到和WithTimeout一样的效果 ,只是一个是按时间点来停,一个是按时间段来停 , 示例代码如下 deadLineTime := time.Now().Add(1500 * time.Millisecond) ctx, cancelctx := context.WithDeadline(ctx, deadLineTime)context.WithValue
context还有个用途是用来记录和传递value, 比如我们可以向别的goroutine传递UserID ,RequestID等要跟踪的变量信息 。 还是通过示例代码来说明
func main() { var ctx = context.Background() key := 1 ctx = context.WithValue(ctx, key, "someValue") doSomething(ctx) } func doSomething(ctx context.Context) { fmt.Printf("Doing something! %s \n", ctx.Value(1)) }输出结果很简单就是"Doing something! someValue" 这个someValue就是通过Context传递给doSomething方法里面的 。 如果ctx.Value的key不存在 ,也不会报错 ,会返回nil.
上面key的写法 ,Visual Studio Code会提示 ”should not use built-in type int as key for value; define your own type to avoid collisions“
就是说这个key可能冲突 ,比如你的程序用String "userID", 别人也用String ”userID“, 你取的时候取到别人用的了 ,那样就给程序带来隐患 , 我们需要自定义key, 如下代码示例 type ctxKey int const ( ctxUserKey ctxKey = iota ctxAuthToken ) func main() { var ctx = context.Background() ctx = context.WithValue(ctx, ctxUserKey, "someValue") doSomething(ctx) } func doSomething(ctx context.Context) { fmt.Printf("Doing something! %s \n", ctx.Value(ctxUserKey)) }我们定义了常量ctxUserKey , 它是ctxKey类型 ,ctxUserKey它的实际值是0 , 在取值和赋值的时候我们同意用ctxUserKey , 这样就能很清楚明白的拿到它的value. 如果我们用ctx.Value(0)是拿不到值的 。
最后
作者在书中说context包的包一直存在争议,为什么呢? 因为它可以放入任意类型 ,这样就让有些偷懒的程序员把它当垃圾桶 , 程序运行需要的参数也用它来传递,这样它就被赋予了太多它不应该的功能 。 那么我们需要遵守一定的规则来使用它 ,作者给出了建议
1, 数据应该通过进程或API边界 , 这句话感觉是翻译问题 ,不是特别清楚 ,我感觉作者的意思是通过明确的参数来传递数据 ,而不是context
2 ,数据应该是不可变的 。 意思就是只WithValue一次 ,不要去修改它
3 ,数据应该趋向简单类型 , 保持简单很重要
4 ,数据应该是数据 ,而不是类型与方法 , 也是保持简单
5,数据应该用于装饰操作 ,而不是驱动操作。意思就是你不要通过context里面的内容来决定你的代码逻辑 。作者最后说context提供的取消功能非常有用 , 就是WithValue不要滥用 。这一章感觉翻译的不好,读起来很晦涩 ,我还是看了另外一篇文章搞明白的。 贴出链接在此https://www.digitalocean.com/community/tutorials/how-to-use-contexts-in-go
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!