golang中Context的使用场景

题目来源:腾讯

答案:

Go1.7加入到标准库,在于控制goroutine的生命周期。当一个计算任务被goroutine承接了之后,由于某种原因(超时,或者强制退出)我们希望中止这个goroutine的计算任务,那么就用得到这个Context了。
包含 CancelContext,TimeoutContext,DeadLineContext,ValueContext
场景一:RPC调用
在主goroutine上有4个RPC,RPC2/3/4是并行请求的,我们这里希望在RPC2请求失败之后,直接返回错误,并且让RPC3/4停止继续计算。这个时候,就使用的到Context。

  1. package main
  2. import (
  3. "context"
  4. "sync"
  5. "github.com/pkg/errors"
  6. )
  7. func Rpc(ctx context.Context, url string) error {
  8. result := make(chan int)
  9. err := make(chan error)
  10. go func() {
  11. // 进行RPC调用,并且返回是否成功,成功通过result传递成功信息,错误通过error传递错误信息
  12. isSuccess := true
  13. if isSuccess {
  14. result <- 1
  15. } else {
  16. err <- errors.New("some error happen")
  17. }
  18. }()
  19. select {
  20. case <- ctx.Done():
  21. // 其他RPC调用调用失败
  22. return ctx.Err()
  23. case e := <- err:
  24. // 本RPC调用失败,返回错误信息
  25. return e
  26. case <- result:
  27. // 本RPC调用成功,不返回错误信息
  28. return nil
  29. }
  30. }
  31. func main() {
  32. ctx, cancel := context.WithCancel(context.Background())
  33. // RPC1调用
  34. err := Rpc(ctx, "http://rpc_1_url")
  35. if err != nil {
  36. return
  37. }
  38. wg := sync.WaitGroup{}
  39. // RPC2调用
  40. wg.Add(1)
  41. go func(){
  42. defer wg.Done()
  43. err := Rpc(ctx, "http://rpc_2_url")
  44. if err != nil {
  45. cancel()
  46. }
  47. }()
  48. // RPC3调用
  49. wg.Add(1)
  50. go func(){
  51. defer wg.Done()
  52. err := Rpc(ctx, "http://rpc_3_url")
  53. if err != nil {
  54. cancel()
  55. }
  56. }()
  57. // RPC4调用
  58. wg.Add(1)
  59. go func(){
  60. defer wg.Done()
  61. err := Rpc(ctx, "http://rpc_4_url")
  62. if err != nil {
  63. cancel()
  64. }
  65. }()
  66. wg.Wait()
  67. }

场景二:PipeLine
runSimplePipeline的流水线工人有三个,lineListSource负责将参数一个个分割进行传输,lineParser负责将字符串处理成int64,sink根据具体的值判断这个数据是否可用。他们所有的返回值基本上都有两个chan,一个用于传递数据,一个用于传递错误。(<-chan string, <-chan error)输入基本上也都有两个值,一个是Context,用于传声控制的,一个是(in <-chan)输入产品的。

我们可以看到,这三个工人的具体函数里面,都使用switch处理了case <-ctx.Done()。这个就是生产线上的命令控制。

  1. func lineParser(ctx context.Context, base int, in <-chan string) (
  2. <-chan int64, <-chan error, error) {
  3. ...
  4. go func() {
  5. defer close(out)
  6. defer close(errc)
  7. for line := range in {
  8. n, err := strconv.ParseInt(line, base, 64)
  9. if err != nil {
  10. errc <- err
  11. return
  12. }
  13. select {
  14. case out <- n:
  15. case <-ctx.Done():
  16. return
  17. }
  18. }
  19. }()
  20. return out, errc, nil
  21. }

场景三:超时请求
我们发送RPC请求的时候,往往希望对这个请求进行一个超时的限制。当一个RPC请求超过10s的请求,自动断开。当然我们使用CancelContext,也能实现这个功能(开启一个新的goroutine,这个goroutine拿着cancel函数,当时间到了,就调用cancel函数)。

鉴于这个需求是非常常见的,context包也实现了这个需求:timerCtx。具体实例化的方法是 WithDeadline 和 WithTimeout。

具体的timerCtx里面的逻辑也就是通过time.AfterFunc来调用ctx.cancel的。

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "time"
  6. )
  7. func main() {
  8. ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
  9. defer cancel()
  10. select {
  11. case <-time.After(1 * time.Second):
  12. fmt.Println("overslept")
  13. case <-ctx.Done():
  14. fmt.Println(ctx.Err()) // prints "context deadline exceeded"
  15. }
  16. }

场景四:HTTP服务器的request互相传递数据
context还提供了valueCtx的数据结构。

这个valueCtx最经常使用的场景就是在一个http服务器中,在request中传递一个特定值,比如有一个中间件,做cookie验证,然后把验证后的用户名存放在request中。

我们可以看到,官方的request里面是包含了Context的,并且提供了WithContext的方法进行context的替换。

  1. package main
  2. import (
  3. "net/http"
  4. "context"
  5. )
  6. type FooKey string
  7. var UserName = FooKey("user-name")
  8. var UserId = FooKey("user-id")
  9. func foo(next http.HandlerFunc) http.HandlerFunc {
  10. return func(w http.ResponseWriter, r *http.Request) {
  11. ctx := context.WithValue(r.Context(), UserId, "1")
  12. ctx2 := context.WithValue(ctx, UserName, "yejianfeng")
  13. next(w, r.WithContext(ctx2))
  14. }
  15. }
  16. func GetUserName(context context.Context) string {
  17. if ret, ok := context.Value(UserName).(string); ok {
  18. return ret
  19. }
  20. return ""
  21. }
  22. func GetUserId(context context.Context) string {
  23. if ret, ok := context.Value(UserId).(string); ok {
  24. return ret
  25. }
  26. return ""
  27. }
  28. func test(w http.ResponseWriter, r *http.Request) {
  29. w.Write([]byte("welcome: "))
  30. w.Write([]byte(GetUserId(r.Context())))
  31. w.Write([]byte(" "))
  32. w.Write([]byte(GetUserName(r.Context())))
  33. }
  34. func main() {
  35. http.Handle("/", foo(test))
  36. http.ListenAndServe(":8080", nil)
  37. }