go context scenarios

golang in the Context of usage scenarios


After Go1.7 context entered in the standard library. It is mainly useful if one sentence, is that the control goroutine life cycle. When a computing task is to undertake a goroutine, for some reason (time-out, or forced to quit) we want to stop this goroutine of computing tasks, then use to get the Context of.

About Context of four structures, CancelContext, TimeoutContext, DeadLineContext, use ValueContext quick grasp Golang context in which a package has been said quite understand.

This paper mainly disk golang in the context of a number of usage scenarios:

Scene One: RPC call

14508334-8ca1c4b9c0515665.png
rpc call graph

There on the main goroutine 4 Ge RPC, RPC2 / 3/4 parallel requests, we hope here after RPC2 request fails, an error is returned directly, and let RPC3 / 4 discontinue the calculation. This time, it used to Context.

The specific implementation in the following code.

package main

import (
    "context"
    "sync"
    "github.com/pkg/errors"
)

func Rpc(ctx context.Context, url string) error {
    result := make(chan int)
    err := make(chan error)

    go func() {
        // 进行RPC调用,并且返回是否成功,成功通过result传递成功信息,错误通过error传递错误信息
        isSuccess := true
        if isSuccess {
            result <- 1
        } else {
            err <- errors.New("some error happen")
        }
    }()

    select {
        case <- ctx.Done():
            // 其他RPC调用调用失败
            return ctx.Err()
        case e := <- err:
            // 本RPC调用失败,返回错误信息
            return e
        case <- result:
            // 本RPC调用成功,不返回错误信息
            return nil
    }
}


func main() {
    ctx, cancel := context.WithCancel(context.Background())

    // RPC1调用
    err := Rpc(ctx, "http://rpc_1_url")
    if err != nil {
        return
    }

    wg := sync.WaitGroup{}

    // RPC2调用
    wg.Add(1)
    go func(){
        defer wg.Done()
        err := Rpc(ctx, "http://rpc_2_url")
        if err != nil {
            cancel()
        }
    }()

    // RPC3调用
    wg.Add(1)
    go func(){
        defer wg.Done()
        err := Rpc(ctx, "http://rpc_3_url")
        if err != nil {
            cancel()
        }
    }()

    // RPC4调用
    wg.Add(1)
    go func(){
        defer wg.Done()
        err := Rpc(ctx, "http://rpc_4_url")
        if err != nil {
            cancel()
        }
    }()

    wg.Wait()
}

Of course, I used here waitGroup main function was to ensure the exit after all the RPC call to complete.

In Rpc functions, the first parameter is a CancelContext, said the image of this Context, is a mouthpiece, in the creation of CancelContext returns a listen to the sound device (ctx) and microphone (cancel function). All goroutine are holding this hearing microphone (ctx), when the main goroutine goroutine want to tell all to end, cancel function by the end of message to tell all of goroutine. Of course, all of goroutine need to deal with this built-in microphone to listen to the end of logic signals (ctx-> Done ()). We can look inside Rpc functions, to judge by a select ctx's done and the current rpc call to which the first end.

This waitGroup and one of the RPC RPC call to inform all logic, in fact, there is a package has helped us to do the work. errorGroup. The use of this particular packet may errorGroup see test examples of this package.

Some people may be concerned that we have here cancel () will be called multiple times, cancel call context package is idempotent. Multiple calls can be assured.

Here we may wish to look at products, where the Rpc functions, in fact, our case there is a request for "blocking" of the request or if you are using http.Get http.Post to achieve, in fact Goroutine Rpc function ended , the actual internal http.Get has not ended. So, the next need to understand the function where the best is "non-blocking", such is http.Do, then you can be interrupted in some way. For example, like this article Cancel http.Request using this example in Context:

func httpRequest(
  ctx context.Context,
  client *http.Client,
  req *http.Request,
  respChan chan []byte,
  errChan chan error
) {
  req = req.WithContext(ctx)
  tr := &http.Transport{}
  client.Transport = tr
  go func() {
    resp, err := client.Do(req)
    if err != nil {
      errChan <- err
    }
    if resp != nil {
      defer resp.Body.Close()
      respData, err := ioutil.ReadAll(resp.Body)
      if err != nil {
        errChan <- err
      }
      respChan <- respData
    } else {
      errChan <- errors.New("HTTP request failed")
    }
  }()
  for {
    select {
    case <-ctx.Done():
      tr.CancelRequest(req)
      errChan <- errors.New("HTTP request cancelled")
      return
    case <-errChan:
      tr.CancelRequest(req)
      return
    }
  }
}

It uses http.Client.Do, then received ctx.Done when to end by calling transport.CancelRequest.
We can also refer to net / dail / DialContext
In other words, if you want your package to achieve is "suspend / controllable", then you realize the function inside your bag, it is best to receive a Context function, and managed Context.Done.

Scene Two: PipeLine

pipeline model is the model line, a few workers on the assembly line, there are n product, a product assembly. In fact, to achieve and is not related Context pipeline model, without a context we can achieve pipeline chan model. However, the control of the entire pipeline, it is required to use on the Context. This article Pipeline Patterns in the example of Go is a very good explanation. Explained here substantially lower this code.

runSimplePipeline three pipeline workers, lineListSource responsible for a divided transmission parameters, lineParser responsible for processing string to int64, sink determines whether the data is available in accordance with specific values. They all are basically two Chan return values, one for data transfer, for a transfer error. (<-Chan string, <-chan error) basically has two input values, is a Context, for controlling sound transmission, one (in <-chan) input products.

We can see that the specific function of the three workers inside, are processed using the switch case <-ctx.Done (). This is the command line control.

func lineParser(ctx context.Context, base int, in <-chan string) (
    <-chan int64, <-chan error, error) {
    ...
    go func() {
        defer close(out)
        defer close(errc)

        for line := range in {

            n, err := strconv.ParseInt(line, base, 64)
            if err != nil {
                errc <- err
                return
            }

            select {
            case out <- n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out, errc, nil
}

Scene Three: Request timed out

We RPC requests, they often want to request a time-out of the restrictions. When a request RPC request exceeds 10s, automatically disconnected. Of course, we use CancelContext, can achieve this function (open a new goroutine, goroutine holding cancel this function, when the time is up, call cancel function).

In view of this demand is very common, context package also implements this requirement: timerCtx. Specific examples of methods and WithDeadline WithTimeout.

Specific timerCtx inside the logic is invoked by time.AfterFunc of ctx.cancel.

Official examples:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }
}

In the http client inside with timeout it is also a common way

uri := "https://httpbin.org/delay/3"
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
    log.Fatalf("http.NewRequest() failed with '%s'\n", err)
}

ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*100)
req = req.WithContext(ctx)

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatalf("http.DefaultClient.Do() failed with:\n'%s'\n", err)
}
defer resp.Body.Close()

In the http server setting a timeout on how to do it?

package main

import (
    "net/http"
    "time"
)

func test(w http.ResponseWriter, r *http.Request) {
    time.Sleep(20 * time.Second)
    w.Write([]byte("test"))
}


func main() {
    http.HandleFunc("/", test)
    timeoutHandler := http.TimeoutHandler(http.DefaultServeMux, 5 * time.Second, "timeout")
    http.ListenAndServe(":8080", timeoutHandler)
}

We take a look inside TimeoutHandler, in essence, is the process by context.WithTimeout do.

func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
  ...
        ctx, cancelCtx = context.WithTimeout(r.Context(), h.dt)
        defer cancelCtx()
    ...
    go func() {
    ...
        h.handler.ServeHTTP(tw, r)
    }()
    select {
    ...
    case <-ctx.Done():
        ...
    }
}

Scene Four: request HTTP server transfer data to each other

context data structure is also provided in valueCtx.

This valueCtx most frequently used scene is in a http server, the transfer request in a particular value, such as a middleware, do cookie authentication, then the user name is verified, it is stored in the request.

We can see that there is an official request contains Context, and provides a method WithContext the context of replacement.

package main

import (
    "net/http"
    "context"
)

type FooKey string

var UserName = FooKey("user-name")
var UserId = FooKey("user-id")

func foo(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := context.WithValue(r.Context(), UserId, "1")
        ctx2 := context.WithValue(ctx, UserName, "yejianfeng")
        next(w, r.WithContext(ctx2))
    }
}

func GetUserName(context context.Context) string {
    if ret, ok := context.Value(UserName).(string); ok {
        return ret
    }
    return ""
}

func GetUserId(context context.Context) string {
    if ret, ok := context.Value(UserId).(string); ok {
        return ret
    }
    return ""
}

func test(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("welcome: "))
    w.Write([]byte(GetUserId(r.Context())))
    w.Write([]byte(" "))
    w.Write([]byte(GetUserName(r.Context())))
}

func main() {
    http.Handle("/", foo(test))
    http.ListenAndServe(":8080", nil)
}

When using ValueCtx to note that, key here should not become a common set of String or Int type, in order to prevent different middleware coverage of this key. Each middleware best case is to use a custom key type, such as FooKey here, but also acquisition logic Value extracted as much as possible as a function, in the same package in this middleware. In this way, it will avoid different packages to set the same key issues of the conflict.

Reprinted to: https://studygolang.com/articles/18316?fr=sidebar

Guess you like

Origin blog.csdn.net/weixin_33957648/article/details/91026072