Golang standard library context Context
In the Go http package Server, each request has a corresponding goroutine to process. Request handlers usually start additional goroutines to access back-end services, such as databases and RPC services. The goroutine used to process a request usually needs to access some data specific to the request, such as the identity authentication information of the end user, the token related to authentication, and the deadline of the request. When a request is cancelled or timed out, all goroutines used to process the request should exit quickly, and then the system can release the resources occupied by these goroutines.
1 Why do I need Context
Basic example
package main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup // Initial example func worker() { for { fmt.Println("worker") time.Sleep(time.Second) } // How to receive external commands to exit wg.Done() } func main() { wg.Add(1) go worker() // How to gracefully implement the ending sub goroutine wg.Wait() fmt.Println("over") }
Global variable mode
package main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup var exit bool // Problems with global variable mode: // 1. Using global variables is not easy to unify when calling across packages // 2. If goroutine is started again in the worker, it is not easy to control. func worker() { for { fmt.Println("worker") time.Sleep(time.Second) if exit { break } } wg.Done() } func main() { wg.Add(1) go worker() time.Sleep(time.Second * 3) // Sleep for 3 seconds to prevent the program from exiting too quickly exit = true // Modify the global variable to realize the exit of the sub goroutine wg.Wait() fmt.Println("over") }
Channel mode
package main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup // Problems in pipeline mode: // 1. It is not easy to achieve standardization and unification when using global variables in cross package calls, and a common channel needs to be maintained func worker(exitChan chan struct{}) { LOOP: for { fmt.Println("worker") time.Sleep(time.Second) select { case <-exitChan: // Waiting for notification from superior break LOOP default: } } wg.Done() } func main() { var exitChan = make(chan struct{}) wg.Add(1) go worker(exitChan) time.Sleep(time.Second * 3) // Sleep for 3 seconds to prevent the program from exiting too quickly exitChan <- struct{}{} // Send exit signal to sub goroutine close(exitChan) wg.Wait() fmt.Println("over") }
Official version of the scheme
package main import ( "context" "fmt" "sync" "time" ) var wg sync.WaitGroup func worker(ctx context.Context) { LOOP: for { fmt.Println("worker") time.Sleep(time.Second) select { case <-ctx.Done(): // Waiting for superior notice break LOOP default: } } wg.Done() } func main() { ctx, cancel := context.WithCancel(context.Background()) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 3) cancel() // Notify the child goroutine to end wg.Wait() fmt.Println("over") }
When a child goroutine opens another goroutine, you only need to pass ctx in:
package main import ( "context" "fmt" "sync" "time" ) var wg sync.WaitGroup func worker(ctx context.Context) { go worker2(ctx) LOOP: for { fmt.Println("worker") time.Sleep(time.Second) select { case <-ctx.Done(): // Waiting for superior notice break LOOP default: } } wg.Done() } func worker2(ctx context.Context) { LOOP: for { fmt.Println("worker2") time.Sleep(time.Second) select { case <-ctx.Done(): // Waiting for superior notice break LOOP default: } } } func main() { ctx, cancel := context.WithCancel(context.Background()) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 3) cancel() // Notify the child goroutine to end wg.Wait() fmt.Println("over") }
2. Context acquaintance
Go1.7 adds a new standard library context, which defines the context type, which is specially used to simplify the data, cancellation signal, deadline and other related operations between multiple goroutine s processing a single request and the request domain. These operations may involve multiple API calls.
Incoming requests to the server should create context, while outgoing calls to the server should accept context. The function call chain between them must pass a context or a derived context that can be created using WithCancel, WithDeadline, WithTimeout, or WithValue. When a context is cancelled, all contexts derived from it are also cancelled.
3. Context interface
context.Context is an interface that defines four methods to be implemented. The signature is as follows:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
Of which:
- The deadline method needs to return the time when the current Context is cancelled, that is, the deadline for completing the work (deadline);
- The Done method needs to return a Channel, which will be closed after the current work is completed or the context is cancelled. Calling the Done method multiple times will return the same Channel;
- The Err method will return the reason why the current Context ends. It will only return a non empty value when the Channel returned by Done is closed;
- If the current Context is cancelled, a cancelled error will be returned;
- If the current Context times out, the DeadlineExceeded error will be returned;
- The Value method will return the Value corresponding to the Key from the Context. For the same Context, calling Value multiple times and passing in the same Key will return the same result. This method is only used to transfer data across API s and between processes and request domains;
4. Background() and TODO()
Go has two built-in functions: Background() and TODO(). These two functions return a background and todo that implement the Context interface respectively. At the beginning of our code, these two built-in Context objects are used as the top-level part Context to derive more sub Context objects.
Background() is mainly used in the main function, initialization and test code as the top-level Context of the tree structure of Context, that is, the root Context.
TODO(), which doesn't know the specific usage scenario at present, can be used if we don't know what Context to use.
background and todo are essentially emptyCtx structure types. They are a Context that cannot be cancelled, has no deadline set, and does not carry any value.
5. With series functions
In addition, four With series functions are defined in the context package.
6. WithCancel
The function signature of WithCancel is as follows:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel returns a copy of the parent node with the new Done channel. When the cancel function returned is called or when the Done channel of the parent context is closed, the Done channel of the returned context is closed, no matter what happens first.
Canceling this context frees the resources associated with it, so code should call cancel immediately after the operation running in this context is completed.
func gen(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done(): return // return end the goroutine to prevent leakage case dst <- n: n++ } } }() return dst } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // When we get the required integer, we call cancel. for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } }
In the example code above, the gen function generates integers in a separate goroutine and sends them to the returned channel. The caller of Gen needs to cancel the context after using the generated integer to avoid leakage of the internal goroutine started by Gen.
7. WithDeadline
The function signature of WithDeadline is as follows:
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
Return a copy of the parent context and adjust the deadline to no later than D. If the deadline of the parent context is earlier than D, then WithDeadline(parent, d) is semantically equivalent to the parent context. When the deadline expires, when the returned cancel function is called, or when the Done channel of the parent context is closed, the Done channel of the returned context will be closed, whichever occurs first.
Canceling this context frees the resources associated with it, so code should call cancel immediately after the operation running in this context is completed.
func main() { d := time.Now().Add(50 * time.Millisecond) ctx, cancel := context.WithDeadline(context.Background(), d) // Although ctx will expire, it is good practice to call its cancel function in any case. // If you don't, you may keep the context and its parent classes alive longer than necessary. defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) } }
In the above code, we define a deadline that expires after 50 milliseconds, and then we call context Withdeadline (context. Background(), d) gets a context (ctx) and a cancel function (cancel), and then uses a select to put the main program into a wait: wait for 1 second and print overslept to exit, or wait for ctx to expire and exit. Because ctx expires in 50 seconds, ctx Done () will receive the value first, and the above code will print ctx Err () cancellation reason.
8. WithTimeout
The function signature of WithTimeout is as follows:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout returns withdeadline (parent, time. Now() Add(timeout)).
Canceling this context will release its related resources, so the code should call cancel immediately after the operation running in this context is completed, which is usually used for timeout control of database or network connection. Specific examples are as follows:
package main import ( "context" "fmt" "sync" "time" ) // context.WithTimeout var wg sync.WaitGroup func worker(ctx context.Context) { LOOP: for { fmt.Println("db connecting ...") time.Sleep(time.Millisecond * 10) // Suppose it takes 10 milliseconds to connect to the database normally select { case <-ctx.Done(): // Automatic call after 50 milliseconds break LOOP default: } } fmt.Println("worker done!") wg.Done() } func main() { // Set a timeout of 50 milliseconds ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 5) cancel() // Notify the child goroutine to end wg.Wait() fmt.Println("over") }
9. WithValue
The WithValue function can establish a relationship between the data of the request scope and the Context object. The statement is as follows:
func WithValue(parent Context, key, val interface{}) Context
WithValue returns the copy of the parent node, where the value associated with the key is val.
Use the context value only for the data passing the request domain between the API and the process, rather than using it to pass optional parameters to the function.
The keys provided must be comparable and should not be of type string or any other built-in type to avoid conflicts between packages using context. Users of WithValue should define their own types for keys. In order to avoid allocation when assigned to interface {}, the context key usually has the specific type struct {}. Alternatively, the static type of the exported context critical variable should be a pointer or interface.
package main import ( "context" "fmt" "sync" "time" ) // context.WithValue type TraceCode string var wg sync.WaitGroup func worker(ctx context.Context) { key := TraceCode("TRACE_CODE") traceCode, ok := ctx.Value(key).(string) // Get the trace code in the sub goroutine if !ok { fmt.Println("invalid trace code") } LOOP: for { fmt.Printf("worker, trace code:%s\n", traceCode) time.Sleep(time.Millisecond * 10) // Suppose it takes 10 milliseconds to connect to the database normally select { case <-ctx.Done(): // Automatic call after 50 milliseconds break LOOP default: } } fmt.Println("worker done!") wg.Done() } func main() { // Set a timeout of 50 milliseconds ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) // Set the trace code in the entry of the system and pass it to the subsequent goroutine to realize the aggregation of log data ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234") wg.Add(1) go worker(ctx) time.Sleep(time.Second * 5) cancel() // Notify the child goroutine to end wg.Wait() fmt.Println("over") }
10. Precautions for using Context
- It is recommended to display and pass Context in the form of parameters
- For function methods with Context as parameter, Context should be the first parameter.
- When passing Context to a function method, do not pass nil. If you don't know what to pass, use Context TODO()
- The Value related method of Context should pass the necessary data of the request field and should not be used to pass optional parameters
- Context is thread safe and can be safely passed in multiple goroutine s
11. Example of client timeout cancellation
How to implement timeout control on the client when calling the server API?
12. server side
// context_timeout/server/main.go package main import ( "fmt" "math/rand" "net/http" "time" ) // server side, random slow response func indexHandler(w http.ResponseWriter, r *http.Request) { number := rand.Intn(2) if number == 0 { time.Sleep(time.Second * 10) // 10 second slow response fmt.Fprintf(w, "slow response") return } fmt.Fprint(w, "quick response") } func main() { http.HandleFunc("/", indexHandler) err := http.ListenAndServe(":8000", nil) if err != nil { panic(err) } }
client side
// context_timeout/client/main.go package main import ( "context" "fmt" "io/ioutil" "net/http" "sync" "time" ) // client type respData struct { resp *http.Response err error } func doCall(ctx context.Context) { transport := http.Transport{ // Frequent requests can define global client objects and enable long links // Request infrequent use of short links DisableKeepAlives: true, } client := http.Client{ Transport: &transport, } respChan := make(chan *respData, 1) req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil) if err != nil { fmt.Printf("new requestg failed, err:%v\n", err) return } req = req.WithContext(ctx) // Create a new client request using ctx with timeout var wg sync.WaitGroup wg.Add(1) defer wg.Wait() go func() { resp, err := client.Do(req) fmt.Printf("client.do resp:%v, err:%v\n", resp, err) rd := &respData{ resp: resp, err: err, } respChan <- rd wg.Done() }() select { case <-ctx.Done(): //transport.CancelRequest(req) fmt.Println("call api timeout") case result := <-respChan: fmt.Println("call server api success") if result.err != nil { fmt.Printf("call server api failed, err:%v\n", result.err) return } defer result.resp.Body.Close() data, _ := ioutil.ReadAll(result.resp.Body) fmt.Printf("resp:%v\n", string(data)) } } func main() { // Define a timeout of 100 milliseconds ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() // Call cancel to release the child goroutine resource doCall(ctx) }