Home > Article > Backend Development > Do you know what are the usage scenarios of Context in golang?
The following column golang tutorial will introduce you to the usage scenarios of Context in golang. I hope it will be helpful to friends in need!
Context usage scenarios in golang
context has been included in the standard library since Go1.7. Its main use, in one sentence, is to control the life cycle of goroutine. When a computing task is taken over by a goroutine, and we want to abort the goroutine's computing task for some reason (timeout, or forced exit), then this Context is used.
This article mainly discusses some usage scenarios of context in golang:
Scenario 1: RPC call
There are 4 on the main goroutine RPC and RPC2/3/4 are requested in parallel. Here we hope that after the RPC2 request fails, an error will be returned directly and RPC3/4 will stop continuing to calculate. At this time, Context is used.
The specific implementation of this is as follows.
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 use waitGroup here to ensure that the main function does not exit until all RPC calls are completed.
In the Rpc function, the first parameter is a CancelContext. This Context is a microphone. When creating the CancelContext, a listener (ctx) and microphone (cancel function) are returned. ). All goroutines hold this listener (ctx). When the main goroutine wants to tell all goroutines that it is about to end, it tells all goroutines the end information through the cancel function. Of course, all goroutines need to have built-in logic to handle the listener end signal (ctx->Done()). We can look inside the Rpc function and use a select to determine which one of ctx's done and the current rpc call ends first.
This waitGroup and one of the RPC calls notify all RPC logic. In fact, a package has already done it for us. errorGroup. For specific usage of this errorGroup package, please see the test example of this package.
Some people may worry that our cancel() here will be called multiple times. The cancel call in the context package is idempotent. You can call it multiple times with confidence.
We might as well take a look here. The Rpc function here is actually a "blocking" request in our example. If this request is implemented using http.Get or http.Post, in fact, The Goroutine of the Rpc function has ended, but the actual http.Get inside has not ended. Therefore, you need to understand that the function here is best to be "non-blocking", such as http.Do, and then it can be interrupted in some way. For example, like this example in this article Cancel http.Request using 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, and then when receiving ctx.Done, it ends by calling transport.CancelRequest.
We can also refer to net/dail/DialContext
In other words, if you want the package you implement to be "stoppable/controllable", then in the function you implement, it is best to Can receive a Context function and handle Context.Done.
Scenario 2: PipeLine
The pipeline model is an assembly line model. Several workers on the assembly line have n products and assemble them one by one. In fact, the implementation of the pipeline model has nothing to do with Context. We can also use chan to implement the pipeline model without context. But for the control of the entire pipeline, Context needs to be used. The example in this article Pipeline Patterns in Go is a very good illustration. Here is a rough description of this code.
There are three pipeline workers in runSimplePipeline. lineListSource is responsible for dividing the parameters one by one for transmission, lineParser is responsible for processing the string into int64, and sink determines whether the data is available based on the specific value. All their return values basically have two chans, one for passing data and one for passing errors. (<-chan string, <-chan error) input basically has two values, one is Context, which is used for voice transmission control, and the other is (in <-chan) input product.
We can see that in the specific functions of these three workers, switch is used to handle case <-ctx.Done(). This is the command control on the production line.
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 }
Scenario 3: Timeout request
When we send an RPC request, we often want to impose a timeout limit on this request. When an RPC request exceeds 10 seconds, it will be automatically disconnected. Of course, we can also achieve this function by using CancelContext (start a new goroutine, this goroutine holds the cancel function, and when the time is up, the cancel function is called).
Given that this requirement is very common, the context package also implements this requirement: timerCtx. The specific instantiation methods are WithDeadline and WithTimeout.
The specific logic in timerCtx is to call ctx.cancel through time.AfterFunc.
Official example:
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" } }
It is also a common method to add timeout in the http client
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()
How to set a timeout on the http server?
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) }
我们看看TimeoutHandler的内部,本质上也是通过context.WithTimeout来做处理。
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(): ... } }
场景四:HTTP服务器的request互相传递数据
context还提供了valueCtx的数据结构。
这个valueCtx最经常使用的场景就是在一个http服务器中,在request中传递一个特定值,比如有一个中间件,做cookie验证,然后把验证后的用户名存放在request中。
我们可以看到,官方的request里面是包含了Context的,并且提供了WithContext的方法进行context的替换。
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) }
在使用ValueCtx的时候需要注意一点,这里的key不应该设置成为普通的String或者Int类型,为了防止不同的中间件对这个key的覆盖。最好的情况是每个中间件使用一个自定义的key类型,比如这里的FooKey,而且获取Value的逻辑尽量也抽取出来作为一个函数,放在这个middleware的同包中。这样,就会有效避免不同包设置相同的key的冲突问题了。
The above is the detailed content of Do you know what are the usage scenarios of Context in golang?. For more information, please follow other related articles on the PHP Chinese website!