How to understand Context in Go?

Time:2024-2-23
Best tutorial I’ve seen so far besides “go language programming”: https://www.practical-go-lessons.com

Original text:https://www.practical-go-lessons.com/chap-37-context

What will you learn in this chapter?
1. What is context?
2. What is a chained table?
3. How do I use the Context Pack?

Technical concepts covered
Context derivation
Linked list
Context key-value pair
Cancellation
Timeout
Deadline

present (sb for a job etc)
The chapter is dedicated to context packages. In the first part of this chapter, we will learn what a “context” is and what its purpose is. In the second part, we will learn how to use context packages in real programs.

What is context-oriented programming?

define

The word context comes from the Latin word “contexo”, which means to combine, connect, link something with a group of other things. In this context, we think of the context of something as a set of links and connections with other things. In everyday language, we usually use the following expressions to describe context:

lit. take meaning from cut segment (idiom); to interpret out of context
in the context of something
Things, behaviors, and words have a context in which they are placed and are connected to other things. If we take things out of context, we simplify them and may misunderstand their true meaning. Context is an important source of information for improved decision making. Part of context includes, but is not limited to, the following:

spot
daytime
histories
people
To better understand how important context is to us, here are some examples:

In a meeting, someone says, “This idea won’t work.” If we only see this statement, we may assume that the idea is completely unworkable. But if we realize that the idea was brought up in a discussion and that there were other people who brought up similar ideas, then we can better understand what the person meant.
In a historical event, we need to understand the political, social and cultural context of the time in order to truly understand the significance and impact of the event.
Therefore, understanding the context is important for us to make the right decisions.

Context can increase understanding of events

Imagine being on a walk and you hear a conversation between two people:
Did you see the game last week?
Yes!
After this, I’m sure they’ll win the next game!
Sure, I’d be willing to bet a thousand bucks they’re talking about “the game.”
A team wins one game last week and has a good chance of winning another next week. We don’t know which team or sport it is. The context of the conversation can help us understand it. If the conversation takes place in New York, we can guess that it has something to do with baseball or basketball because those sports are very popular there. If the conversation takes place in Paris, the likelihood that they are talking about soccer is very high. What we are doing here is adding context to understand something. Here we talk about the place. We can also add a time element to the context of the conversation. If we know when things happened, we will be able to browse through the results of the week’s sporting events to get a better understanding.

Context changes behavior

An analysis of the context of an event can change the behavior of participants. Consider the context when answering the following questions:

Do your manners differ when you are in your own country and when you are in another country?
Do you use the same level of language in the office as you do when communicating with your family?
Do you dress more formally than usual for an interview?
The answer may be “no” because our behavior is influenced by our environment. Specific situations affect the way we act, and the environment affects our behaviors and reactions.

Context in Computer Science

Typically, we design computer programs to perform predefined tasks. The specified routine we implement always executes in the same way and does not change depending on the user who uses it. Even if the environment changes, the behavior of the program does not change. The idea of context-oriented programming, on the other hand, is to introduce changes in a program that are influenced by the context.In 1999, Abowd proposed an interesting definition of context: “Context is any information we can use to describe the situation of an entity. An entity is a person, place, or object that is considered relevant to the interaction between a user and an application, including the user and the application itself.” Implicit and explicit information are the building blocks of context. Programmers should consider context to build applications that can adjust their behavior at runtime. So what is intelligence? The word “intelligence” comes from the Latin root “intellego,” which means to discern, unravel, notice, recognize. Something is intelligent if it can recognize and understand it. Introducing context into apps doesn’t make them smart, but they do tend to make them aware of their environment and users.

“Context” package: history and use cases
histories

This package was first developed internally by Google developers. It has been introduced into the Go standard library. Until then, it is available in the Go subrepository.

use case

The context package has two main uses:

Cancellation of dissemination

To understand this usage, let’s take the example of a fictional construction company called FooBar. The city of Paris commissioned this company to build a huge swimming pool. The mayor of Paris defends its idea among the representatives of the population and the project is approved. The company begins the project; the project manager has ordered all the raw materials needed to build the pool. Four months pass, the mayor changes and the project is canceled! FooBar’s project manager is furious; the company has to cancel 156 orders. He started joining them one by one over the phone. Some of them also ordered raw materials from other construction companies. Everyone is suffering from the rapid evolution of this situation. Now let’s assume that the project manager did not cancel the orders from the subcontractors. The other companies will produce the required goods but will not be paid. This is a huge waste of resources. As you can see in the figure below, the cancellation of the project is spreading to all the workers indirectly involved. The city council canceled the project; FooBar canceled its order to the contractor. In construction and other human activities, there are always ways to cancel work. We can use the context package to introduce a cancellation policy into our program. When a request is made to a Web server, we can cancel all chains of work if the client disconnects!How to understand Context in Go?

Data transfer in the call stack

When a request is made to a Web server, the Web server function responsible for handling the request will not do the job alone. The request will go through a series of functions and methods and then a response will be sent. A single request can generate new requests to other microservices in the microservices architecture! This
function callchaincall stack. We will see in this section why it is useful to transfer data along with the call stack. We will give another example: developing a Web server for a shopping application. We have a user who interacts with our application.

Users will access the login using their web browser;
page to fill in their login details;
The Web browser will send an authentication request to the server, which will forward the request to the authentication service;
The server will build the My Account page (e.g. via a template) and send the user a response.
If a user requests a “Last Order” page, the server will need to call the order service to retrieve them.
How to understand Context in Go?

What data can we add to the context?

We can keep the type of device that sent the request in context.
If the device is a cell phone, we can choose to load a lightweight template to improve the user experience.
The order service can also load only the last five orders to reduce page rendering time.

We can keep the ID of the authenticated user in the context.
We can also keep the IP of incoming requests.
It can be used by the authentication layer to block suspicious activity (introduction of block lists, detection of errors, multiple login attempts)
Another very common use case is the generation of individual request IDs. requestId is passed to each layer of the application. With the ID, the team responsible for maintenance is able to track the request in the logs. (Distributed Link Trace

Setting deadlines and timeouts

Deadline: the time by which a task should be completed. Timeout: is a very similar concept. Instead of considering the exact date and time in the calendar, we consider the maximum duration allowed. We can use context to define time limits for long running processes. Here is an example:

You develop a server and the client specifies a timeout of 1 second.
A context with a timeout of 1 second can be set; after this 1 second, the client will disconnect.
In this case, we again wish to avoid wasting resources.

connector

The context package exposes an interface consisting of four methods:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

In the next section, we’ll learn how to use the package

linked list

The context package is built with a standard data structure: a chained table. In order to fully understand how contexts work, we first need to understand chained lists. A linked list is a collection of data elements. The type of data stored in a list is unlimited; it can be an integer, a string, a structure, a floating-point number …… and so on. Each element of a list is a node. Each node contains two things:

data value
The address in memory of the next element in the list. In other words, it is a pointer to the next value.

You can see a visual representation of a linked list in Figure 3. The list is “linked”, and the nodes in the list have a child (the next element in the list) and a parent (the last element in the list). Note that this is not true; the first node in the list has no parent. It is the root, the origin, the head of the list. There is another noteworthy exception to this rule; the final node does not have any children.
How to understand Context in Go?

Root context: Background

In most programs, we create a context.Background() in the root of the program. For example, in the main function that will start our application. To create the root context, you can use the following syntax.

ctx := context.Background()

A call to the Background() function will return a pointer to an empty context. Internally, a call to Background() will create a new context.emptyCtx. this type is not disclosed: this type is not disclosed:

type emptyCtx int

The base type of emptyCtx is int. This type implements the four methods required by the Context interface:

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil
}

func (*emptyCtx) Err() error {
    return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
    return nil
}

Note that the emptyCtx type also implements the interface fmt.Stringer. this allows us to perform fmt.Println(ctx):

fmt.Println(reflect.TypeOf(ctx))
// *context.emptyCtx
fmt.Println(ctx)
// context.Background

Adding context to your functions/methods

After creating the root context, we can pass it to a function or method. But before that, we have to add a context parameter to the function:

func foo1(ctx context.Context, a int) {
    //...
}

In the previous list, you noted two Go idioms that are widely used in go projects:

The context is the first argument of the function, the
The context parameter is named ctx.

derived context

We created the root context in the previous section. This context is empty; it does nothing. What we can do is derive another subcontext from the empty context:How to understand Context in Go?

To derive a context, you can use the following functions:

WithCancel
WithTimeout
WithDeadline
WithValue

WithCancel

The WithCancel function accepts only one function namedparent of the argument. This argument represents the context we want to export. We’ll create a new context, and the parent context will retain a reference to this new child context. Let’s look at the signature of the WithCancel function:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

This function returns the next subcontext and CancelFunc, which is a custom type of the context package:

type CancelFunc func()

CancelFunc is a named type whose underlying type is func(). This function “tells the operation to give up its job” (Golang source). Calling WithCancel will give us the means to cancel the operation. Here’s how to create a derived context:

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

To cancel the operation, you need to call cancel:

cancel()

WithTimeout / WithDeadline

Timeout: is the maximum time required for a process to complete normally. For any process that takes a variable amount of time to execute, we can add a timeout, which is a fixed amount of time we are allowed to wait. Without a timeout, our application can wait indefinitely for the process to complete.

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

Deadline: is a specified point in time. When you set a deadline, you specify that the process will not exceed that deadline.

deadline := time.Date(2021, 12, 12, 3, 30, 30, 30, time.UTC)
ctx, cancel := context.WithDeadline(context.Background(), deadline)

give an example

The absence of context

As an example: we will design an application that must make HTTP requests to a web server to get data and then display it to the user. We will first consider the application without a context and then add a context to it. Client

package main

import (
    "log"
    "net/http"
)

func main() {
    req, err := http.NewRequest("GET", "http://127.0.0.1:8989", nil)
    if err != nil {
        panic(err)
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    log.Println("resp received", resp)
}

Here we have a simple http client. We create a GET request that will call “http 8989”. If we can’t create the request, our program panics. We then use the default HTTP client (http.DefaultClient) to send the request to the server (using method Do). The received response is then printed to the user. Server Side

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        log.Println("request received")
        time.Sleep(time.Second * 3) // Wait for 3s
        fmt.Fprintf(w, "Response") // send data to server
        log.Println("response sent")

    })
    err := http.ListenAndServe("127.0.0.1:8989", nil) // set listen port
    if err != nil {
        panic(err)
    }
}

The code is very simple. We first set up the http handler using function.http.HandleFunc. This function takes two arguments: the path and the function that responds to the request. We use the time.Sleep(time.Second * 3) instruction to wait 3 seconds and then write the response. This sleep is to fake the time it takes for the server to answer. In this case, the response is just a “response”. We then start the server to listen on 127.0.0.1:8989 (localhost, port 8989). Test First, we start the server; then, we start the client. 3 seconds later, the client receives a response.

$ go run server.go
2019/04/22 12:17:11 request received
2019/04/22 12:17:14 response sent

$ go run client.go
2019/04/22 12:17:14 resp received &{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[8] Content-Type:[text/plain; charset=utf-8] Date:[Mon, 22 Apr 2019 10:17:14 GMT]] 0xc000132180 8 [] false false map[] 0xc00011c000 <nil>}

As you can see, our client has to deal with a delay of 3 seconds. Let’s add a condition to the server: let’s say we sleep for 1 minute now. Our client will wait 1 minute; it will block our application for 1 minute. We can note here that our client application is doomed to wait for the server, even if it takes an infinite amount of time. This is not a good design. Users will not be happy to wait indefinitely for an application to answer. In my opinion, it’s better to tell the user that something is wrong rather than making him wait indefinitely.

Client Context

We will keep the base of the previously created code. We will start by creating the root context:

rootCtx := context.Background()

We then export this context to a new context called ctx:

ctx, cancel := context.WithTimeout(rootCtx, 50*time.Millisecond)

The function WithTimeout takes two arguments: the context and time.Duration.
The second parameter is the timeout duration.
Here we set it to 50 milliseconds.
I recommend that you create a configuration variable in your actual application to save the timeout duration. By doing this, you do not need to recompile the program to change the timeout.
context.WithTimeout will return:

derived context
Cancel function

The cancel function warns a child process that it should abort the operation it is performing. Calling cancel will release the resources associated with the context. To ensure that the cancel function is called at the end of the program, we will use the defer statement:

defer cancel()

The next step consists of creating the request and attaching our brand new context to it:

req, err := http.NewRequest("GET", "http://127.0.0.1:8989", nil)
if err != nil {
    panic(err)
}
// Add context to our request
req = req.WithContext(ctx)

The other lines are the same as the version without context. Complete client code:

// context/client-side/main.go
package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func main() {
    rootCtx := context.Background()
    req, err := http.NewRequest("GET", "http://127.0.0.1:8989", nil)
    if err != nil {
        panic(err)
    }
    // create context
    ctx, cancel := context.WithTimeout(rootCtx, 50*time.Millisecond)
    defer cancel()
    // attach context to our request
    req = req.WithContext(ctx)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    fmt.Println("resp received", resp)
}

Now let’s test our new client. Here are the server logs:

2019/04/24 00:52:08 request received
2019/04/24 00:52:11 response sent

We see that we received a request and sent a response 3 seconds later. Here are the logs from our client:

panic: Get http://127.0.0.1:8989: context deadline exceeded

We see that http.DefaultClient.Do returns an error.

The text states that the deadline has been exceeded.
Our request has been canceled because our server took 3 seconds to complete its work. Even though the client canceled the request, the server continues to perform its work. We have to find a way to share the context between the client and the server.

server-side context

Header

An HTTP request consists of a set of headers, body, and query string. When we send a request, Go does not transmit any information about the context of the request. If you want to visualize the request’s headers, you can add the following line to the server code:

fmt.Println("headers :")
for name, headers := range r.Header {
    for _, h := range headers {
        fmt.Printf("%s: %s\n", name, h)
    }
}

We use a loop to iterate through the requested headers and print them. Here are the headers transmitted with our client:

headers :
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip

We only have two headings. The first one provides more information about the client used. The second informs the server that the client can accept gzip compressed data. Nothing to do with the final timeout. But if we look at the http.Request object, we can notice that there is a method called Context(). This method will retrieve the context of the request. If it is not defined yet, it will return an empty context:

func (r *Request) Context() context.Context {
    if r.ctx != nil {
        return r.ctx
    }
    return context.Background()
}

The documentation says that “the context will be canceled when the client connection is closed”. This means that inside the go server implementation, the cancel function is called when the client connection is closed. This means that inside our server, we have to listen to the channel returned by ctx.Done(). When we receive a message on that channel, we must stop what we are currently doing.

doWork function

Let’s see how we can introduce this into our server. For example, we will introduce a new function, doWork, which will represent the compute-intensive tasks that our server handles. This doWork is a placeholder for CPU-intensive operations.How to understand Context in Go?

We will start the doWork function in a separate goroutine. This function takes as arguments the context and the channel to write the result to. Let’s look at the code for this function:

// context/server-side/main.go 
//...

func doWork(ctx context.Context, resChan chan int) {
    log.Println("[doWork] launch the doWork")
    sum := 0
    for {
        log.Println("[doWork] one iteration")
        time.Sleep(time.Millisecond)
        select {
        case <-ctx.Done():
            log.Println("[doWork] ctx Done is received inside doWork")
            return
        default:
            sum++
            if sum > 1000 {
                log.Println("[doWork] sum has reached 1000")
                resChan <- sum
                return
            }
        }
    }

}<font color = black>

In Figure 5, you can see the activity diagram of the doWork function. In this function, we will use channels to communicate with the caller. We create a for loop and place a select statement inside that loop. In this select statement, we have two cases:

The channel returned by ctx.Done() is closed. This means that we receive the command to finish the job.
In this case, we will interrupt the loop, record a message and return.

Default (executed if the previous case is not executed)
In this default case, we will increase the sum.
If the variable sum becomes strictly greater than 1.000, we will send the result on the result channel (resChan).
How to understand Context in Go?

Server-side processing

// context/server-side/main.go 
//...

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        log.Println("[Handler] request received")
        // Fetch the context of the request
        rCtx := r.Context()
        // Create a result acceptance channel
        resChan := make(chan int)
        // Load an asynchronous program
        go doWork(rCtx, resChan)
        // Wait for
        // 1. Client missing link.
        // 2. Function completion
        select {
        case <-rCtx.Done():
            log.Println("[Handler] context canceled in main handler, client has diconnected")
            return
        case result := <-resChan:
            log.Println("[Handler] Received 1000")
            log.Println("[Handler] Send response")
            fmt.Fprintf(w, "Response %d", result) // send data to client side
            return
        }
    })
    err := http.ListenAndServe("127.0.0.1:8989", nil) // set listen port
    if err != nil {
        panic(err)
    }
}

We changed the handler code to use the request context. The first thing to do here is to retrieve the request context:

rCtx := r.Context()

Then we set up an integer channel (resChan) that allows you to communicate with the doWork function. We’ll start the doWork function in a separate goroutine.

resChan := make(chan int)
// launch the function doWork in a goroutine
go doWork(rCtx, resChan)

We will then use the select statement to wait for two possible events:

The client closes the connection; therefore, the cancel channel will be closed.
The function doWork has done its job. (We received an integer from the resChan channel)
In option 1, we record a message and return it. When option 2 occurs, we use the result of the resChan channel and write it to the response writer. Our client will receive the result computed by the doWork function. Let’s run our server and client. In Figure 6 you can see the execution logs of the client and server programs. You can see that the handler receives the request and then starts the doWork function. The handler then receives a cancel signal. The signal is then propagated to the doWork function.
How to understand Context in Go?

WithDeadline

WithDeadline and WithTimeout are very similar. If we look at the source code of the context package, we can see that the function WithTimeout is just a wrapper around WithDeadline:

// source : context.go (in the standard library)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

If you look at the previous code snippet, you can see that the timeout duration has been added to the current time. Let’s take a look at the signature of the WithDeadline function:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

This function takes two arguments:

parent context
Specific time.

usage

As we said in the previous section, deadline and timeout are similar concepts. A timeout is expressed as a duration, while a deadline is expressed as a specific point in timeHow to understand Context in Go?

WithDeadline can be used where WithTimeout is used. Here is an example from the standard library:

// golang standard library
// src/net/dnsclient_unix.go
// line 133

// exchange sends a query on the connection and hopes for a response.
func (r *Resolver) exchange(ctx context.Context, server string, q dnsmessage.Question, timeout time.Duration) (dnsmessage.Parser, dnsmessage.Header, error) {
    //....
    for _, network := range []string{"udp", "tcp"} {
        ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
        defer cancel()

        c, err := r.dial(ctx, network, server)
        if err != nil {
            return dnsmessage.Parser{}, dnsmessage.Header{}, err
        }
        //...
    }
    return dnsmessage.Parser{}, dnsmessage.Header{}, errNoAnswerFromDNSServer
}

Here the exchange function takes the context as its first argument.
For each network (UDP or TCP), it derives the context passed as an argument.
The input context is derived by calling context.WithDeadline. The deadline is created by adding the timeout duration to the current time: time.Now().Add(timeout)
Note that immediately after the creation of the derived context, the call to the cancel function returned by context.WithDeadline is delayed. This means that the cancel function will be called when the exchange function returns.
For example, if the dial function returns an error for some reason, the exchange function will return, the cancel function will be called, and the cancel signal will propagate to the subcontext.

Cancellation of dissemination

This section will delve into the mechanics of cancel propagation. Let’s take an example:

func main(){
    ctx1 := context.Background()
    ctx2, c := context.WithCancel(ctx1)
    defer c()
}

In this applet, we first define a root context: ctx1. We then derive this context by calling context.WithCancel. go will create a new structure. The function that will be called is as follows:

// src/context/context.go

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}

A struct cancelCtx is created and our root context is embedded in it. Here is the type struct cancelCtx:

// src/context/context.go

type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects the following fields
    done     chan struct{}         // created lazily, closed by the first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}

We have five fields:

Context (parent) is an embedded field (it has no explicit field name)
Mutually Exclusive Lock (named mu)
Channel named done
The field named children is a map. the key is of type canceller, the value is of type struct{}
There is also an error called err

Canceller is an interface:

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

Types that implement interface cancelers must implement functions: cancel functions and completion functions.
How to understand Context in Go?

What happens when we execute the cancel function? ctx2 What happens?

The mutex (mu) will be locked. Therefore, no other goroutine can modify this context.
The channel (completion) will be closed
All sub-levels of ctx2 will also be canceled (in this case, we have no sub-levels…)
The mutex will be unlocked.

bi-dissemination
How to understand Context in Go?

func main() {
    ctx1 := context.Background()
    ctx2, c2 := context.WithCancel(ctx1)
    ctx3, c3 := context.WithCancel(ctx2)
    //...
}

Here we create ctx3, a new object of type cancelCtx. The child context ctx3 will be added to the parent context (ctx2). The parent context ctx2 will keep the memory of its children. Currently, it has only one child ctx3. Now let’s see what happens when we call the cancel function c2.

The mutex (mu) will be locked. Therefore, no other goroutine can modify this context.
The channel (completion) will be closed
All sub-levels of ctx2 will also be canceled (in this case, we have no sub-levels…) ctx3 will be canceled by the same procedure
ctx1 (parent of ctx2) is an empty Ctx, so ctx2 is not removed from ctx1.
The mutex will be unlocked.

triple transmission

func main() {
    ctx1 := context.Background()
    ctx2, c2 := context.WithCancel(ctx1)
    ctx3, c3 := context.WithCancel(ctx2)
    ctx4, c4 := context.WithCancel(ctx3)
}

How to understand Context in Go? How to understand Context in Go?
As you can see in the figure, we have a root context and three descendants.
The last one is ctx4 When we call c2, it will cancel ctx2 and its children (ctx3).
When ctx3 is canceled, it will also cancel all its children and ctx4 will be canceled.
The key message in this section is “When you cancel a context, the cancel operation is propagated from parent to child”.

An important idiomatic use: defer cancel()

The following two lines of code are common:

ctx, cancel = context.WithCancel(ctx)
defer cancel()

You may encounter these lines in the standard library, but also in many libraries. Once we derive the existing context, we call the cancel function in the defer statement. As we saw earlier, the cancel instruction propagates from parent to child; why do we need to explicitly call cancel?When building a library, you’re not sure if anyone will effectively execute the cancel function in the parent context. By adding a call to cancel in a deferred statement, you can ensure that cancel is called:

When the function returns (or reaches the end of its body)
or when the goroutine running the function panics.

The Goroutine leak

To understand this phenomenon, let’s take an example. First, we define two functions: doSth and doSth2. these two functions are virtual. They take the context as their first argument. Then they wait indefinitely for the channel returned by ctx.Done() close:

func doSth2(ctx context.Context) {
    select {
    case <-ctx.Done():
        log.Println("second goroutine return")
        return
    }
}

func doSth(ctx context.Context) {
    select {
    case <-ctx.Done():
        log.Println("first goroutine return")
        return
    }
}

We will now use these two functions in a third function called launch:

func launch() {
    ctx := context.Background()
    ctx, _ = context.WithCancel(ctx)
    log.Println("launch first goroutine")
    go doSth(ctx)
    log.Println("launch second goroutine")
    go doSth2(ctx)
}

In this function, we first create a root context (returned by context.Background). Then we export this root context. We call the WithCancel() method to get the context that can be canceled. Then we start two goroutines. now let’s look at our main function:

// context/goroutine-leak/main.go 
// ...

func main() {
    log.Println("begin program")
    go launch()
    time.Sleep(time.Millisecond)
    log.Printf("Gouroutine count: %d\n", runtime.NumGoroutine())
    for {
    }
}

We start the function in the goroutine. Then we pause it (1 millisecond) and count the number of goroutines. A very handy function is defined in the runtime package:

runtime.NumGoroutine()

The number of goroutines here should be 3: 1 main goroutine + 1 goroutine that executes doSth + 1 goroutine that executes doSth2. If we don’t call cancel, the last two goroutines will run indefinitely. Note that we have created another goroutine in the program: the goroutine that started launch. this goroutine will not be counted because it will return almost immediately. When we cancel the context, both of our goroutines will return. So the number of goroutines will be reduced to 1 (the main coprocessor). But here, we don’t call the cancel function at all.

2019/05/04 19:01:16 begin program
2019/05/04 19:01:16 launch first goroutine
2019/05/04 19:01:16 launch second goroutine
2019/05/04 19:01:16 Gouroutine count: 3

In the main function, we can’t cancel the context (because it’s defined in the launch function). We have 2 leaking goroutines! To fix this, we just need to modify the function launch and add a delay statement:

func launch() {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()
    log.Println("launch first goroutine")
    go doSth(ctx)
    log.Println("launch second goroutine")
    go doSth2(ctx)
}

2019/05/04 19:15:09 begin program
2019/05/04 19:15:09 launch first goroutine
2019/05/04 19:15:09 launch second goroutine
2019/05/04 19:15:09 first goroutine return
2019/05/04 19:15:09 second goroutine return
2019/05/04 19:15:09 Gouroutine count: 1

WithValue

Contexts can carry data. This feature is intended to be used with request-scoped data, for example:

Credentials (e.g., JSON Web tokens)
Request id (for tracking requests in the system)
Requested IP
Some headers (e.g. user agent)

give an example

/

/ context/with-value/main.go 
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"

    uuid "github.com/satori/go.uuid"
)

func main() {
    http.HandleFunc("/status", status)
    err := http.ListenAndServe(":8091", nil)
    if err != nil {
        log.Fatal(err)
    }
}

type key int

const (
    requestID key = iota
    jwt
)

func status(w http.ResponseWriter, req *http.Request) {
    // Add the request ID to the ctx
    ctx := context.WithValue(req.Context(), requestID, uuid.NewV4().String())
    // Add authentication credentials to ctx
    ctx = context.WithValue(ctx, jwt, req.Header.Get("Authorization"))


    upDB, err := isDatabaseUp(ctx)
    if err != nil {
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }
    upAuth, err := isMonitoringUp(ctx)
    if err != nil {
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }
    fmt.Fprintf(w, "DB up: %t | Monitoring up: %t\n", upDB, upAuth)
}


func isDatabaseUp(ctx context.Context) (bool, error) {
    // Remove the requestId
    reqID, ok := ctx.Value(requestID).(string)
    if !ok {
        return false, fmt.Errorf("requestID in context does not have the expected type")
    }
    log.Printf("req %s - checking db status", reqID)
    return true, nil
}

func isMonitoringUp(ctx context.Context) (bool, error) {
    // Remove the requestId
    reqID, ok := ctx.Value(requestID).(string)
    if !ok {
        return false, fmt.Errorf("requestID in context does not have the expected type")
    }
    log.Printf("req %s - checking monitoring status", reqID)
    return true, nil
}

We have created a server that is listening to localhost:8091
This server has only one route: “/status”
We use ctx := context.WithValue(req.Context(), requestID, uuid.NewV4().String()) to derive the request context (req.Context())
We add a key-value pair to the context: requestID

Then we update the operation. We add a new key pair to hold the request credentials: ctx = context.WithValue(ctx, jwt, req.Header.Get(“Authorization”))
Context values can then be accessed through isDatabaseUp and isMonitoringUp:

reqID, ok := ctx.Value(requestID).(string)
if !ok {
    return false, fmt.Errorf("requestID in context does not have the expected type")
}

★ Note: In the process of link tracing, the better way to print the log is to use traceID as the resident field of the log, then store the subobjects of the log into the ctx, and when you need to use the log to print later, you can take out the log instance from the ctx to print the log.

Key type

func WithValue(parent Context, key, val interface{}) Context

The parameters key and val are of type interface{}. In other words, they can have any type. Only one restriction should be observed, namely that the key type should be comparable. I

They can share the context between multiple packages.
You may want to restrict access to context values external to the package to which the value is added.
To do this, you can create an unexported type
All keys are of this type.
We will define the keys globally within the package:

type key int

const (
    requestID key = iota
    jwt
)

In the previous example, we created a type key with the base type int (comparable). We then defined two unexported global constants. These constants are then used to add values and retrieve values from the context:

/

/ add a value
ctx := context.WithValue(req.Context(), requestID, uuid.NewV4().String())

// get a value
reqID, ok := ctx.Value(requestID).(string)

Expected missing values and types different from actual types

Value returns nil when the key-value pair is not found in the context.
That’s why we make type assertions: to protect us from missing values or values that don’t have the desired type.

Recommended Today

[Spring]SpringBoot Unified Function Processing

Article Catalog preamble1. Interceptor1.1 What is an interceptor1.2 Use of interceptors1.2.1 Customizing Interceptors1.2.2 Registering a Configuration Interceptor 1.3 Interceptor Explained1.3.1 Intercepting paths1.3.2 Interceptor Execution Flow1.3.3 Adapter model 2. Harmonization of data return formats3. Harmonization of exception handling preamble In the daily use of the Spring framework for development, for some boards, you may need to […]