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 ).
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.
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 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 }
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.
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.
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))
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") } }
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) }
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
.
Context
structure does not have a cancel method, because only the function that spawns the context should cancel it.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