Y said

Weekends are always short lived.

It’s a nice, sunny day today. I went to the nearby shopping mall to have a meal of “Gaolaojiu Chongqing hot pot”, which tasted ok, mainly because I haven’t had hot pot for a long time ~

During the day to clean up the home, feel the mood also changed.

Golang has been in daily development for several months. As a Golang newbie, some things tend to be used without much time to dig into the rationale and design implications behind them. This year SILENTLY set a Flag for myself, is to study this language deeply.

When using Golang, we find that many downstream frameworks or services often require us to pass in a context. context object, which is usually in the first argument of the function. In our company’s framework, each request has a unique Log Id that is used to concatenate multiple microservice requests. Sometimes our own code may not need this object, but in order to keep the call chain intact and the log is not lost, it has to be passed on.

Let’s start with coroutines

One of the great things about Golang is that it has a powerful concurrency tool: Goroutine. It is a Golang language coroutine, a single machine can support a large number of concurrent requests at the same time, very suitable for the Internet era of back-end services.

So there’s a lot of coroutines, and that creates some problems. For example: how do some of the more general parameters of the request (such as the Log Id mentioned above) get passed to the coroutine? How do you terminate a coroutine?

In Golang, we cannot terminate a coroutine externally, only by itself. For common requirements such as timeout cancellation, we usually use preemption or interrupt subsequent operations.

Golang does this in channel + select mode until the context comes out. The subcoroutine starts a timed task loop to listen on the channel. The main coroutine writes signals to the channel if it wants to cancel the subcoroutine.

That does solve the problem, but not to mention the coding hassle, if you have coroutines that start coroutines, that form a coroutine tree, you have to define a lot of channels.

The Context of the interface

Context is an interface in the Context package. Its interface definition is very simple:

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}}Copy the code

Briefly explain how these four methods work:

  • Done: Returns a Channel to indicate whether the current coroutine is finished.
  • Err: Returns why the context was canceled when the Done Channel ends. Returns if it is cancelledCanceled; Returns if it is timed outDeadlineExceeded;
  • Deadline: Returns the time when the context will be cancelled. If no time is set, ok returns false.
  • Value: Retrieves data related to the context.

The default Context implementation

The context package has some default context implementations, which can be used in almost any application scenario. Here is a brief introduction:

emptyCtx

The implementation of emptyCtx is a variable of type int that has no timeout, cannot be cancelled, and cannot store any additional information.

It has two instances: Background and TODO, which are returned by two methods. Background is usually used in main functions, initializations, and tests, as a top-level context, which means we create a context based on Background; TODO is used when you’re not sure what context to use.

valueCtx

ValueCtx can store key-value pairs. And there’s a combination that points to the parent Context. The code is as follows:

type valueCtx struct {
    Context
    key, val interface{}}func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")}if! reflect.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}
Copy the code

The WithValue method can use the passed context as the parent, add a key-value pair, and then create a new context. When you’re looking for a Value, you’re going to look up the context tree, so if you can’t find it in the current context, you’re going to try to find it in its parent context, so it’s kind of like a chain of responsibility.

cancelCtx

Cancelable context. It also defines a Canceler interface in its own package. Structure:

type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by 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
}

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

The key is the cancel method, which sets the cancellation reason, cancels all children, and removes the current node from the parent if necessary.

The WithCancel function is used to create a cancelable context, which is of type cancelCtx. WithCancel returns a context and a CancelFunc. CancelFunc triggers the cancel operation.

type CancelFunc func(a)

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    // Add the current context to the children of the ancestor node whose latest type is cancelCtx
    propagateCancel(parent, &c)
    return &c, func(a) { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
    // generate a new child node with parent as context
    return cancelCtx{Context: parent}
}
Copy the code

Notice the propagateCancel method here, why the nearest ancestor node and not the parent node? Since its parent may not be a cancelCtx, it may be a valueCtx or something, there is no children field.

timerCtx

TimerCtx is a context that can be cancelled at a scheduled time. It is internally designed based on cancelCtx and also implements the cancel interface.

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    CancelCtx cancels internal cancelCtx
    c.cancelCtx.cancel(false, err)
    if removeFromParent {
        // Remove this timerCtx from its parent cancelCtx's children.
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    ifc.timer ! =nil{cancel the timer c.imer.Stop() c.imer =nil
    }
    c.mu.Unlock()
}
Copy the code

WithDeadline returns a parent-based timerCtx with an expiration deadline no later than the set time d. The logic is as follows:

  1. If the parent nodeparentThere is an expiration time and the expiration time is earlier than the given timed, then the newly created child nodecontextYou do not need to set the expiration timeWithCancelCreate a cancelablecontextCan;
  2. Otherwise, useparentAnd expiration timedCreate a scheduled canceltimerCtxAnd create a new onecontextAnd can be cancelledcontextThe ancestor node is disassociated, and then the current time is determined to expiredThe length of thedur:
  3. ifdurIf the value is less than 0, the new one is cancelledtimerCtx, the reason forDeadlineExceeded;
  4. Otherwise, it is a new onetimerCtxSet the timer to cancel the current once the expiration time is reachedtimerCtx.

Similar to WithDeadline, WithTimeout creates a context that is timed out, except that WithDeadline receives an expiration time, whereas WithTimeout receives an expiration time relative to the current time

use

The coroutine itself needs to listen on the Done method’s channel to decide whether to terminate the coroutine:

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

// consumer
go func(ctx context.Context) {
    ticker := time.NewTicker(1 * time.Second)
    for _ = range ticker.C {
        select {
            case <-ctx.Done():
            	fmt.Println("child process interrupt...")
            	return
            default:
            	fmt.Printf("send message: %d\n", <-messages)
        }
    }
}(ctx)
Copy the code

In the parent coroutine, cancel is signaled by defining timeout or manually calling cancel().

Those of you who have read the net/ HTTP package source code may have noticed that context is used when implementing HTTP Server.

  1. First, the server creates one when it starts the servicevalueCtx, stores information about the server, and each subsequent connection opens a coroutine and carries thisvalueCtx.
  2. After the connection is established, it will be based on incomingcontextTo create avalueCtxUsed to store local address information, and then created another one on top of thatcancelCtx, and then starts reading the network request from the current connection, which is read each time a request is readcancelCtxPassed in to pass the cancel signal. Once the connection is disconnected, a cancel signal is sent to cancel all network requests in progress.
  3. Once the request is read, it is again based on the incomingcontextCreate a newcancelCtxAnd set to the current request objectreqAt the same timeresponseIn the objectcancelCtxSave the currentcontextCancel method.

Step 3 May be a little clearer in code:

ctx, cancelCtx := context.WithCancel(ctx)
req.ctx = ctx
// omit other methods
w = &response{
    conn:          c,
    cancelCtx:     cancelCtx,
    req:           req,
    reqBody:       req.Body,
    handlerHeader: make(Header),
    contentLength: - 1,
    closeNotifyCh: make(chan bool.1),

    // We populate these ahead of time so we're not
    // reading from req.Header after their Handler starts
    // and maybe mutates it (Issue 14940)
    wants10KeepAlive: req.wantsHttp10KeepAlive(),
    wantsClose:       req.wantsClose(),
}
Copy the code

This design has the following effects:

  • Once the request times outcancelCtxTo interrupt the current request;
  • If an error occurs during the process of constructing response, you can directly call the object of ResponsecancelCtxMethod to end the current request;
  • After processing builds response, the response object is calledcancelCtxMethod to end the current request.

Summary & daily development

Context is mainly used for synchronization cancellation signals between parent and child tasks, which is essentially a way of coroutine scheduling. There are two other things to note when using context:

  1. Upstream tasks are only usedcontextNotifying the downstream task that it is no longer needed does not directly interfere with or interrupt the execution of the downstream task. The downstream task decides on its own subsequent processing operations, that iscontextThe cancel operation is non-intrusive;
  2. contextIt’s thread safe becausecontextIs itself immutable (immutable), so it can be safely passed across multiple coroutines.

As you can see, the most powerful thing about Context is that you can gracefully close coroutines. In a normal service framework, this would be something the framework would do for us, setting a context after receiving the request, passing it into the request’s coroutine, and calling cancel to close the request in the event of timeout or an error. It is important to note that the request coroutine is usually terminated by the framework writing code.

But if we open a coroutine ourselves in the request, the framework code can’t close it. So we’re going to pass in the context, and then in this new coroutine, depending on the nature of this coroutine, see if we’re going to listen for the Done method of the context. Typically, a service has a uniform timeout setting (say, 10 seconds), but if the service triggers a timed task, does that timed task have its own timeout? For example, 10 minutes. If so, you would call WithTimeout for the coroutine to set up a separate context and then listen inside the coroutine.

The context design reminds me of the interrupt of a Java thread, which is just setting up a semaphore, and it’s up to the thread to decide whether or not to interrupt, depending on the context. Before also wrote a Java thread interrupt aspects of the article, interested partners can be in the public number history inside a turn over.

Note that the official recommendation is to pass the context through the call stack, not into a structure. See article: go.dev/blog/contex…

Because context has to be passed through layers of functions, some of you might find it difficult to code. One approach mentioned in a company in-house course is to use A Java-like ThreadLocal to store a context and retrieve it when needed. There’s some hacking involved, like getting the ID of the Goroutine from the stack. But I personally don’t recommend that, because ThreadLocal has been around for a long time when you’re designing the context. Why Golang chose the current design instead of that approach should have some meaning. Golang’s context design follows Golang’s own philosophy of functional programming, and using ThreadLocal feels like a bit of a nonentity.

Context also has the ability to pass values. Currently, we only use it to pass log Id on the team. Can we also use it to pass current operator information? I think it is ok, everyone can use it according to their own team specifications

reference

  • Zhihu – In-depth understanding of Golang context
  • Go Language Chinese language – Golang context package interpretation
  • Go Language Chinese – server development tool Golang Context usage details
  • Go Concurrency Patterns: Context
  • context-and-structs

And a support

My name is Yasin, a blogger who insists on original technology. My wechat official account is: Made a Program

All see here, if feel my article write also ok, might as well support once.

The first article will be sent to the public account, the best reading experience, welcome your attention.

Your every retweet, attention, like, comment is the biggest support for me!

There are learning resources, and frontline Internet companies within the push oh