📜 ⬆️ ⬇️

Understanding the Context package in Golang

image


The context package in Go is useful for interactions with APIs and slow processes, especially in production-grade systems that deal with web requests. With it, you can notify the goroutines of the need to complete their work.


Below is a small guide to help you use this package in your projects, as well as some of the best practices and pitfalls.


(Note lane: Context is used in many packages, for example, in working with Docker ).


Before you start


To use contexts, you must understand what goroutine and channels are. I will try to consider them briefly. If you are already familiar with them, go directly to the Context section.


Gorutin


The official documentation says that “Gorutin is a lightweight stream of execution.” Goroutines are lighter than threads, so managing them is relatively less resource intensive.


→ Sandbox


package main import "fmt" // ,   Hello func printHello() { fmt.Println("Hello from printHello") } func main() { //   //       go func(){fmt.Println("Hello inline")}() //     go printHello() fmt.Println("Hello from main") } 

If you run this program, you will see that only Hello from main printed. In fact, both goroutines start, but main finishes earlier. So, the Goroutines need a way to inform main about the end of their execution, and so that she waits for this. Here channels come to our aid.


Channels


Channels are a way of communication between goroutines. They are used when you want to transfer results, errors or other information from one goroutine to another. Channels are of various types, for example, a channel of type int receives integers, and a channel of type error receives errors, etc.


Say we have a ch channel of type int . If you want to send something to the channel, the syntax will be ch <- 1 . You can get something from the channel like this: var := <- ch , i.e. take the value from the channel and save it in the var variable.


The following code illustrates how to use the channels to confirm that the goroutines have completed their work and returned their values ​​to main .


Note: Waiting groups can also be used for synchronization, but in this article I selected channels for code examples, since we will use them later in the section on contexts.


→ Sandbox


 package main import "fmt" //       int   func printHello(ch chan int) { fmt.Println("Hello from printHello") //     ch <- 2 } func main() { //  .       make //       : // ch := make(chan int, 2),       . ch := make(chan int) //  .  ,    . //       go func(){ fmt.Println("Hello inline") //     ch <- 1 }() //     go printHello(ch) fmt.Println("Hello from main") //      //     ,    i := <- ch fmt.Println("Received ",i) //      //    ,      <- ch } 

Context


The context package in go allows you to pass data to your program in some kind of “context”. The context, like a timeout, deadline or channel, signals a shutdown and calls return.


For example, if you make a web request or execute a system command, it would be a good idea to use a timeout for production-grade systems. Because if the API you are accessing is slow, you are unlikely to want to accumulate requests on your system, as this can lead to increased load and decreased performance when processing your own requests. The result is a cascade effect.


And here the timeout or deadline context may be just right.


Context creation


The context package allows you to create and inherit context in the following ways:


context.Background () ctx Context


This function returns an empty context. It should be used only at a high level (in the main or top-level request handler). It can be used to get other contexts, which we will discuss later.


 ctx, cancel := context.Background() 

context.TODO () ctx Context


This function also creates an empty context. And it should also be used only at a high level or when you are not sure which context to use, or if the function does not yet receive the desired context. This means that you (or someone who supports the code) plan to add context to the function later.


 ctx, cancel := context.TODO() 

Interestingly, take a look at the code , it is absolutely the same as background. The only difference is that in this case, you can use static analysis tools to check the validity of the context transfer, which is an important detail, since these tools help identify potential errors at an early stage and can be included in the CI / CD pipeline.


From here :


 var ( background = new(emptyCtx) todo = new(emptyCtx) ) 

context.WithValue (parent Context, key, val interface {}) (ctx Context, cancel CancelFunc)


Note Lane: There is an inaccuracy in the original article, the correct signature for context.WithValue will be as follows:


 context.WithValue(parent Context, key, val interface{}) Context 

This function takes a context and returns a context derived from it in which the val value is associated with key and passes through the entire context tree. That is, as soon as you create a WithValue context, any derived context will receive this value.


It is not recommended to pass critical parameters using context values; instead, functions should accept them explicitly in the signature.


 ctx := context.WithValue(context.Background(), key, "test") 

context.WithCancel (parent Context) (ctx Context, cancel CancelFunc)


It gets a little more interesting here. This function creates a new context from the parent passed to it. The parent can be the background context or the context passed as an argument to the function.


The derived context and undo function are returned. Only the function that creates it should call the function to cancel the context. You can pass the undo function to other functions if you want, but this is strongly discouraged. Typically, this decision is made from a misunderstanding of the context cancellation. Because of this, contexts generated from this parent can affect the program, which will lead to an unexpected result. In short, it is better to NEVER pass a cancel function.


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

Note Lane: In the original article, the author, apparently, erroneously for context.WithCancel gave an example with context.WithDeadline . The correct example for context.WithCancel would be:


 ctx, cancel := context.WithCancel(context.Background()) 

context.WithDeadline (parent Context, d time.Time) (ctx Context, cancel CancelFunc)


This function returns a derived context from its parent, which is canceled after a deadline or call to the cancel function. For example, you can create a context that is automatically canceled at a specific time and passes this on to child functions. When this context is canceled after the deadline, all functions that have this context should be completed by notification.


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

context.WithTimeout (parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)


This function is similar to context.WithDeadline. The difference is that the length of time is used as input. This function returns a derived context that is canceled when the cancel function is called or after a time.


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

Reception and use of contexts in your functions


Now that we know how to create contexts (Background and TODO) and how to generate contexts (WithValue, WithCancel, Deadline, and Timeout), let's discuss how to use them.


In the following example, you can see that a function that takes a context starts goroutine and expects it to return or cancel the context. The select statement helps us determine what happens first and terminate the function.


After closing the channel Done <-ctx.Done() , the case case <-ctx.Done(): is selected. As soon as this happens, the function should interrupt work and prepare for a return. This means that you must close any open connections, free up resources, and return from the function. There are times when the release of resources may delay the return, for example, the cleanup hangs. You must keep this in mind.


The example that follows this section is a fully finished go program that illustrates timeouts and undo functions.


 // ,  -      // ,   -    func sleepRandomContext(ctx context.Context, ch chan bool) { //  (. .:  )    //     // ,    defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //   sleeptimeChan := make(chan int) //       //     go sleepRandom("sleepRandomContext", sleeptimeChan) //  select        select { case <-ctx.Done(): //   ,    //  ,     -   //    ,    ( ) //    -  , //    ,   //         fmt.Println("Time to return") case sleeptime := <-sleeptimeChan: //   ,       fmt.Println("Slept for ", sleeptime, "ms") } } 

Example


As we saw, using contexts you can work with deadlines, timeouts, and also call the cancel function, thereby making it clear to all functions using a derived context that you need to complete your work and complete return. Consider an example:


main function:



doWorkContext function:



sleepRandomContext function:



sleepRandom function:



This example uses sleep mode to simulate a random processing time, but in reality, you can use channels to signal this function about the start of cleaning and wait for confirmation from the channel that the cleaning is completed.


Sandbox (It looks like the random time I use in the sandbox is practically unchanged. Try this on your local computer to see randomness)


→ Github


 package main import ( "context" "fmt" "math/rand" "Time" ) //   func sleepRandom(fromFunction string, ch chan int) { //    defer func() { fmt.Println(fromFunction, "sleepRandom complete") }() //    //   , // «»      seed := time.Now().UnixNano() r := rand.New(rand.NewSource(seed)) randomNumber := r.Intn(100) sleeptime := randomNumber + 100 fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms") time.Sleep(time.Duration(sleeptime) * time.Millisecond) fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms") //   ,     if ch != nil { ch <- sleeptime } } // ,       // ,   -    func sleepRandomContext(ctx context.Context, ch chan bool) { //  (. .:  )    //     // ,    defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //   sleeptimeChan := make(chan int) //       //     go sleepRandom("sleepRandomContext", sleeptimeChan) //  select        select { case <-ctx.Done(): //   ,    //  ,    doWorkContext  // doWorkContext  main  cancelFunction //  ,     -   //    ,    ( ) //    -  , //    ,   //         fmt.Println("sleepRandomContext: Time to return") case sleeptime := <-sleeptimeChan: //   ,       fmt.Println("Slept for ", sleeptime, "ms") } } //  ,         //       //   ,      main func doWorkContext(ctx context.Context) { //          - //  150  //  ,   ,   150  ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond) //         defer func() { fmt.Println("doWorkContext complete") cancelFunction() }() //       //         , //      ,    ch := make(chan bool) go sleepRandomContext(ctxWithTimeout, ch) //  select      select { case <-ctx.Done(): //   ,           //     ,   main   cancelFunction fmt.Println("doWorkContext: Time to return") case <-ch: //   ,       fmt.Println("sleepRandomContext returned") } } func main() { //   background ctx := context.Background() //     ctxWithCancel, cancelFunction := context.WithCancel(ctx) //      //        defer func() { fmt.Println("Main Defer: canceling context") cancelFunction() }() //     - //   ,        go func() { sleepRandom("Main", nil) cancelFunction() fmt.Println("Main Sleep complete. canceling context") }() //   doWorkContext(ctxWithCancel) } 

Underwater rocks


If the function uses context, make sure that cancellation notifications are handled properly. For example, that exec.CommandContext does not close the read channel until the command completes all forks created by the process ( Github ), i.e. that canceling the context does not immediately return from the function if you wait with cmd.Wait (), until all forks of the external command complete processing.


If you use a timeout or deadline with a maximum runtime, it may not work as expected. In such cases, it is better to implement timeouts using time.After .


Best practics


  1. context.Background should only be used at the highest level, as the root of all derived contexts.
  2. context.TODO should be used when you are not sure what to use, or if the current function will use context in the future.
  3. Context cancellations are recommended, but these functions may take some time to clear and exit.
  4. context.Value should be used as sparingly as possible and should not be used to pass optional parameters. This makes the API incomprehensible and can lead to errors. Such values ​​should be passed as arguments.
  5. Do not store contexts in a structure; pass them explicitly in functions, preferably as the first argument.
  6. Never pass a nil context as an argument. If in doubt, use TODO.
  7. The Context structure does not have a cancel method, because only the function that spawns the context should cancel it.

From translator


In our company, we actively use the Context package when developing server applications for internal use. But such applications for normal functioning, in addition to Context, require additional elements, such as:



Therefore, at some point, we decided to summarize all our experience and created auxiliary packages that greatly simplify writing applications (especially applications that have APIs). We put our developments in the public domain and anyone can use them. The following are some links to packages useful for solving such problems:



Also read other articles on our blog:



')

Source: https://habr.com/ru/post/461723/


All Articles