Video information

How to correctly use package context by Jack Lindamood at Golang UK Conf. 2017

Video: www.youtube.com/watch?v=-_B… Blog: medium.com/@cep21/how-…

Why do WE need Context

  • Each long request should have a timeout limit
  • You need to pass this timeout in the call
    • For example, when we start processing requests, we say a 3-second timeout
    • So how much time does this timeout have left in the middle of a function call?
    • Where does this information need to be stored so that request processing can be stopped

If you think about it further.

This is the normal way, but what happens if the RPC 2 call fails?

After RPC 2 failed, if there was no Context, we might still wait for all RPCS to complete. However, since RPC 2 failed, other RPC results are meaningless and we still need to return an error to the user. So we wasted 10ms and there was no need to wait for other RPCS to complete.

What if we failed at RPC 2 and simply returned a failure to the user?

So the ideal situation would be something like the figure above. When RPC 2 fails, in addition to returning an error message to the user, we should also have some way to notify RPC 3 and RPC 4 to stop running and not waste resources.

So the solution is:

  • Signal a request to stop
  • Contains hints about when a request is likely to end (timeout)
  • Use channel to notify that the request has ended

So let’s just throw the variables out there. 😈

  • There is no thread/Go routine variable in Go
    • Which makes sense, because it makes goroutine dependent on each other
  • It’s very easy to abuse

Context implementation details

The context. The context:

  • Is an immutable tree node
  • Cancel A node that cancels all of its children (from top to bottom)
  • Context values is a node
  • A Value lookup is a way to go back up a tree (from bottom to top)

Sample the Context chain

The complete code: play.golang.org/p/ddpofBV1Q…

package main
func tree() {
  ctx1 := context.Background()
  ctx2, _ := context.WithCancel(ctx1)
  ctx3, _ := context.WithTimeout(ctx2, time.Second * 5)
  ctx4, _ := context.WithTimeout(ctx3, time.Second * 3)
  ctx5, _ := context.WithTimeout(ctx3, time.Second * 6)
  ctx6 := context.WithValue(ctx5, "userID"12)},Copy the code

If the Context chain is formed like this, it looks like this:

When a 5-second timeout is reached:

context.Context API

There are basically two types of operations:

  • Three functions to specify when your child exits;
  • Function to set the variables of the request category
typeDeadline() (Deadline time.time, Ok bool) Done() <-chan struct{} Err() errorCopy the code

When should you use Context?

  • Every RPC call should have the ability to timeout out, which is a reasonable API design
  • It’s not just timeouts, you need to be able to end actions that no longer need to be done
  • Context.Context is the Go standard solution
  • Any function that might block, or take a long time to complete, should have a context.context

How do YOU create a Context?

  • At the beginning of RPC, use context.background ()
    • Some people record a context.background () in main(), put it in a variable on the server, and then inherit the context from that variable when the request comes in. It’s wrong to do that. Direct each request from its own context.background ().
  • If you don’t have a context and need to call a function of context, use context.todo ().
  • If an operation requires its own timeout setting, give it a separate sub-context (as in the previous example)

How does it integrate into the API?

  • If I have a Context,Let’s make that the first variable.
    • Func (d* Dialer) DialContext(CTX context. context, network, address String) (Conn, error)
    • Some people put the context in one of the variables in the middle, which is very unorthodox, don’t do that, put it in the first one.
  • Use it as aoptionalRequest struct.
    • For example, func (r *Request) WithContext(CTX context.context) *Request
  • Please use CTX for Context variable name (no weird names 😓)

The Context where?

  • Think of the Context as a river flowing through your program (another meaning is don’t drink from the river… 🙊)
  • Ideally, the Context exists in the Call Stack
  • Don’t store the Context in a struct
    • Unless you’re using the Request structure in http.Request
  • The request structure should terminate with the end of the request
  • Unreference to Context variables should be removed when the RPC request is processed.
  • Request ends, Context should end. These two are a couple, not wishing to be born on the same day, but wishing to die on the same day… 💕)

Context package considerations

  • Get in the habit of closing Context
    • Especially Contexts that are timed out
  • If a context is GC instead of cancelled, you’ve probably done something wrong
ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2)
defer cancel()
Copy the code
  • Using Timeout causes internal use of time.afterfunc, which causes the context not to be garbage collected until the timer expires.
  • It is a good practice to defer Cancel () immediately after the setup.

Request Cancellation

It’s possible to Cancel a Context when you don’t care about what you’re getting next, right?

As an example, golang.org/x/sync/errgroup errgroup use Context to provide termination behavior of RPC.

type Group struct {
	cancel  func()
	wg      sync.WaitGroup
	errOnce sync.Once
	err     error
}
Copy the code

Create a group and context:

func WithContext(ctx context.Context) (*Group, context.Context) {
  ctx, cancel := context.WithCancel(ctx)
  return &Group{cancel: cancel}, ctx
}
Copy the code

This returns a group that can be cancelled ahead of time.

Instead of calling go func() directly, you call go (), pass in the function as an argument, and call it as a higher-order function, inside which go func() opens the Goroutine.

func (g *Group) Go(f func() error) {
  g.wg.Add(1)
  go func() {
    defer g.wg.Done()
    iferr := f(); err ! = nil { g.errOnce.Do(func() {
        g.err = err
        ifg.cancel ! = nil { g.cancel() } }) } }() }Copy the code

When f returns an error, sync.once is used to cancel the context, and the error is stored in g.err and returned in the subsequent Wait() function.

func (g *Group) Wait() error {
  g.wg.Wait()
  ifg.cancel ! = nil { g.cancel() }return g.err
}
Copy the code

Note: Here, cancel() is called once after Wait().

package main
func DoTwoRequestsAtOnce(ctx context.Context) error {
  eg, egCtx := errgroup.WithContext(ctx)
  var resp1, resp2 *http.Response
  f := func(loc string, respIn **http.Response) func() error {
    return func() error {
      reqCtx, cancel := context.WithTimeout(egCtx, time.Second)
      defer cancel()
      req, _ := http.NewRequest("GET", loc, nil)
      var err error
      *respIn, err = http.DefaultClient.Do(req.WithContext(reqCtx))
      if err == nil && (*respIn).StatusCode >= 500 {
        return errors.New("unexpected!")}return err
    }
  }
  eg.Go(f("http://localhost:8080/fast_request", &resp1))
  eg.Go(f("http://localhost:8080/slow_request", &resp2))
  return eg.Wait()
}
Copy the code

In this example, two RPC calls are made at the same time, and when either call times out or fails, the other RPC call is terminated. Here, the errgroup mentioned above is used to achieve this pattern, which is very convenient to use when there are many non-requests, and it is necessary to deal with timeout and error termination of other concurrent tasks.

Context.Value – Value of the Request category

Plenty of duct tape for context.value API

There’s plenty of duct tape for mending just about anything, from a broken suitcase to a human wound to a car engine to even the Apollo 13 of a NASA moon mission (Yeah! True Story). So in Western culture, duct tape is “everything”. In Chinese, I’m afraid it’s a better word for everything from headaches and fever to colds and fevers to bruises and injuries.

Of course, the subtext of both eastern and western cultures is the same. Context.Value is something of this nature to an API that can do anything but treat the symptoms.

  • The value node is a node in the Context chain
package context
type valueCtx struct {
  Context
  key, val interface{}
}
func WithValue(parent Context, key, val interface{}) Context {
  //  ...
  return &valueCtx{parent, key, val}
}
func (c *valueCtx) Value(key interface{}) interface{} {
  if c.key == key {
    return c.val
  }
  return c.Context.Value(key)
}
Copy the code

You can see that WithValue() is actually adding a node to the Context tree.

Context is immutable.

Constraint key space

To prevent duplicate keys in a tree structure, it is recommended to constrain key space. For example, use private types and then use GetXxx() and WithXxxx() to manipulate private entities.

type privateCtxType string
var (
  reqID = privateCtxType("req-id")
)
func GetRequestID(ctx context.Context) (int, bool) {
  id, exists := ctx.Value(reqID).(int)
  return id, exists
}
func WithRequestID(ctx context.Context, reqid int) context.Context {
  return context.WithValue(ctx, reqID, reqid)
}
Copy the code

We use WithXxx instead of SetXxx because Context is actually immutable, so instead of modifying a value in the Context, we create a new Context with a value.

The Context. The Value is immutable

It is not too much to emphasize that context. Value is immutable.

  • Context.Context is designed in the immutable mode
  • Also, context. Value is immutable
  • Don’t try to store a mutable Value in context. Value and then change it, expecting the other Context to see the change
    • And don’t expect to store mutable values in context. Value. There’s no risk of multiple goroutines accessing the Context concurrently, because it was designed to be immutable all along
    • If you set a timeout, don’t assume you can change the timeout value
  • Keep this in mind when using context. Value

What should I put in context.value?

  • The value of the Request category should be saved
    • Anything about Context itself is Request category.
    • Derived from Request data and terminated with the end of the Request

What doesn’t fall under Request?

  • Created outside of Request and does not change as Request changes
    • For example, the stuff you create in func main() is obviously not in the Request category
  • Database connection
    • What if the User ID is in the connection? (More on that later)
  • Global logger
    • What if the Logger needs a User ID? (More on that later)

So what’s the problem with using context.value?

  • Unfortunately, everything seems to derive from requests
  • So why do we need function arguments? And then why don’t we just have one Context?
func Add(ctx context.Context) int {
  return ctx.Value("first").(int) + ctx.Value("second").(int)
}
Copy the code

I’ve seen an API that looks like this:

func IsAdminUser(ctx context.Context) bool {
  userID := GetUser(ctx)
  return authSingleton.IsAdmin(userID)
}
Copy the code

This is where the API internally gets the UserID from the context, and then determines the permissions. But looking at the function signature, it is completely impossible to understand exactly what the function needs and does.

Code should be designed with readability as a priority.

Instead of getting bogged down in the details of a function’s implementation and reading it line by line, someone will scan the function’s interface first. So a clear functional interface design will make it easier for others (or yourself in a few months) to understand the code.

A good API design should clearly understand the logic of the function from the function signature. If we change the above interface to:

func IsAdminUser(ctx context.Context, userID string, authenticator auth.Service) bool
Copy the code

We can clearly know from this function signature:

  • This function can probably be cancelled ahead of time
  • This function requires a User ID
  • This function requires an authenticator
  • And since authenticator is passed in as an argument, rather than relying on something implicit, we know that when testing it is easy to pass in a mock authentication function to do the testing
  • The userID is the incoming value, so we can change it without worrying about affecting anything else

All of this information is obtained from the function signature without having to open the function implementation to read it line by line.

What can be put in context.value?

Now knowing context. Value makes the interface definition more ambiguous and seems like it shouldn’t be used. So that brings us back to the original question, what exactly can you put in context.value? Another way to think about it is, what’s not derived from Request?

  • Context.Value should be something that informs properties, not something that controls properties
  • Should never be written into a document as input data that must exist
  • If you find that your function doesn’t work properly in some context. Value, that means that the information in that context. Value should not be in it, but should be in the interface. Because I’ve made the interface too fuzzy.

What is not a controlling property?

  • Request ID
    • It simply calls each RPC with an ID and has no practical meaning
    • It’s just a number/string, and you wouldn’t use it as a logical judgment anyway
    • In general, when logging, you need to record it
      • And Logger itself is not a Request category, so logger shouldn’t be in the Context
      • Loggers that are not in the Request category should only use Context information to modify the log
  • User ID (if only for logging)
  • Incoming Request ID

What is obviously controlling in nature?

  • Database connection
    • It obviously affects the logic very badly
    • So this should be explicitly stated in the parameters of the function
  • Authentication Service
    • Obviously, different authentication services lead to different logic
    • You should also put it in the parameters of the function, explicitly

example

Debug context. Value – net/ HTTP /httptrace

medium.com/@cep21/go-1…

package main
func trace(req *http.Request, c *http.Client) {
  trace := &httptrace.ClientTrace{
    GotConn: func(connInfo httptrace.GotConnInfo) {
      fmt.Println("Got Conn")
    },
    ConnectStart: func(network, addr string) {
      fmt.Println("Dial Start")
    },
    ConnectDone: func(network, addr string, err error) {
      fmt.Println("Dial done")
    },
  }
  req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
  c.Do(req)
}
Copy the code

How does NET/HTTP use HttpTrace?

  • If a trace exists, the trace callback is executed
  • This is just an information property, not a control property
    • HTTP does not have different execution logic based on the presence or absence of trace
    • This is just to inform users of the API to help them log or debug
    • So the trace here is in the Context
package http
func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
  //  ...
  trace := httptrace.ContextClientTrace(req.Context())
  //  ...
  iftrace ! = nil && trace.WroteHeaders ! = nil { trace.WroteHeaders() } }Copy the code

Avoid dependency injection – github.com/golang/oauth2

  • Here, it’s weird to use ctx.Value to locate dependencies
  • This is not recommended
    • This is basically just for testing purposes
package main
import "github.com/golang/oauth2"
func oauth() {
  c := &http.Client{Transport: &mockTransport{}}
  ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
  conf := &oauth2.Config{ /* ... */ }
  conf.Exchange(ctx, "code")}Copy the code

Reasons why people abuse context.value

  • Middleware abstraction
  • Deep function call stack
  • Chaotic design

Context. Value doesn’t make your API cleaner, that’s an illusion, but it makes your API definitions more ambiguous.

Conclusion the Context. The Value

  • Very convenient for debugging
  • Putting the necessary information into context.value makes the interface definition even more opaque
  • If you can be as explicit as possible in the interface
  • Try not to use context.value

Conclusion the Context

  • All long, blocking operations require Context
  • Errgroup is a nice abstraction built on top of the Context
  • When the Request ends, Cancel the Context
  • Context.value should be used to inform things of nature, not to control things of nature
  • Constrain the key space of context. Value
  • Context and context.value should be immutable and thread-safe
  • The Context should die as the Request dies

Q&A

Does the database access also use Context?

As mentioned earlier, long, blocking operations use Context, as do database operations. However, for timeout Cancel operations, write operations are generally not cancelled; For read operations, however, there is usually a Cancel operation.

The original

Blog.lab99.org/post/golang…

Personal wechat official Account:

Individual making:

github.com/jiankunking

Personal Blog:

jiankunking.com