Context design is a controversial topic in Golang. Context is not a silver bullet. While it solves some problems, it also has some shortcomings that make people criticize. This article discusses the advantages and disadvantages of context and some suggestions for using it.

disadvantages

Since SUBJectively I’m not a big fan of the context design either, let’s start with the weaknesses.

Everywhere,context

According to the official advice used by context, context should appear on the first argument of the function. This leads directly to the fact that there are contexts all over the code. As a function caller, you must pass a placeholder — context.background () or context.todo () — even if you do not intend to use the context’s functions. This is definitely code smell, especially for clean programmers, passing so many meaningless parameters is simply unacceptable.

Err()Actually, it’s chicken

The context. context interface defines the Err() method:

type Context interface {
    ...
	// If Done is not yet closed, Err returns nil.
	// If Done is closed, Err returns a non-nil error explaining why:
	// Canceled if the context was canceled
	// or DeadlineExceeded if the context's deadline passed. // After Err returns a non-nil error, successive calls to Err return the same error. Err() error ... }Copy the code

When cancellation is triggered (which usually means some error or exception has occurred), you can use the Err() method to see what caused the error. This is a common requirement, but the implementation of Err() is a bit tricky in the context package. Err() is limited to two types of error messages:

  1. Excuse me??
  2. Cancelled due to timeout
// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled")

// DeadlineExceeded is the error returned by Context.Err when the context's // deadline passes. var DeadlineExceeded error = deadlineExceededError{} type deadlineExceededError struct{} func (deadlineExceededError) Error() string { return "context deadline exceeded" } func (deadlineExceededError) Timeout() bool { return true } func (deadlineExceededError) Temporary() bool { return true }Copy the code

You can hardly get any business-relevant error information from the Err() method, which means that if you want to know the specific cancellation reason, you can’t count on the context package, you have to do it yourself. It would be better if the cancel() method received an error:

	ctx := context.Background()
	c, cancel := context.WithCancel(ctx)
	err := errors.New("some error"Cancel (err) // Cancel with the reason for the errorCopy the code

context.ValueFreedom without restraint is dangerous

Context. Value is almost a map[interface{}]interface{} :

type Context interface {
    ...
	Value(key interface{}) interface{}
    ...
}
Copy the code

This gives programmers a great deal of freedom to play almost anything they want. But this almost unfettered freedom can be dangerous. Not only is it open to abuse, misuse, and the loss of compile-time type checking, requiring us to do type assertions on every Value in context.Value in case of panic. Although the documentation states that context.Value should be used to hold “request-scoped” data, there are a thousand definitions of what “request-scoped” means in a thousand people’s eyes. Data like request-id,access_token, and user_id can be treated as “request-scoped” in context.Value, and can be defined in a structure in a more clearly defined way.

Poor readability

Poor readability is also the price of freedom, and seeing the context can be a headache when learning to read the Go code. If the documentation is not clearly commented, it is almost impossible to know what context.Value contains, let alone how to use it properly. The following code defines and comments the context in the HTTP. Request structure:

// http.Request 
typeRequest struct { .... // ctx is either the client or server context. It should only // be modified via copying the whole Request using WithContext. // It is unexported to prevent people from using Context wrong // and mutating the contexts held by callers  of the same request. ctx context.Context }Copy the code

Can you see what the context.Value will hold?

. funcmain () {
    http.Handle("/", http.handlerfunc (func(resp http.responsewriter, req * http.request) {fmt.println (req.context ())) }}))Copy the code

Writing here, I can not help but think of the soul of “sugar brother” torture: these glasses of wine on the table, which one is Maotai?

Even if you print out the context, you don’t know the relationship between the context and the function’s parameters, and maybe the next time you pass another set of parameters, the values inside the context change. In this case, if the document is not clear (unfortunately I have found that most code does not comment context.value clearly), you have to search context.withvalue globally, line by line.

advantages

Although I have a subjective “bias” against context, objectively, it has some merits and merits.

Unified cancelation implementation method

Many articles say that context solves the goroutine cancelation problem, but in fact, I think the implementation of cancelation itself is not a problem. It is relatively simple to implement cancelation using the broadcast feature of closing a channel, for example:

// Cancel triggers a Cancel func Cancel(c chan struct{}) {select {caseClose default: close(c)}} // DoSomething does some time-consuming operations that can be cancelled by cancel. func DoSomething(cancel chan struct{}, arg Arg) { rs := make(chan Result) gofunc() {
		// doSomething rs < -xxx}() select {case <-cancel:
		log.Println("Cancelled.")
	case result := <-rs:
		log.Println("Processing completed")}}Copy the code

Alternatively, you can put the cancellation channel inside the structure:

typeStruct {Arg Arg cancel chan struct{} // cancel channel} // NewTask create a NewTask func NewTask(Arg Arg) *Task{return&task {Arg: Arg, cancel:make(chan struct{}),}} // Cancel triggers a cancel func (t *Task)Cancel() {
	select {
	caseClose default: close(t.c)}} // DoSomething does some time-consuming operations that can be cancelled by cancel. func (t *Task)DoSomething() {
	rs := make(chan Result)
	go func() {
		// do something
		rs <- xxx
	}()
	select {
	case <-t.cancel:
		log.Println("Cancelled.")
	case result := <-rs:
		log.Println("Processing completed")
	}
}
// t := NewTask(arg)
// t.DoSomething()
Copy the code

It can be seen that the realization of Cancelation is also varied. A thousand programmers might write a thousand implementations. However, thanks to the context that unifies the implementation of cancelation, you would have to learn more about its cancelation mechanism every time you reference a library. I think that’s the greatest strength and credit of the context. Gopher will know how to cancel a function whenever they see a context in it. If you want to cancelation, you give priority to the context.

Provides a less elegant, but efficient way to pass values

Context.Value is a double-edged sword, and its disadvantages are mentioned above. However, if used properly, disadvantages can be turned into advantages. The map[interface{}]interface{} attribute determines that it can hold almost anything, so if a method needs to be able to cancelation and receive any data passed by the caller, context.Value is a very useful way to do this. Please refer to the following tips for proper use.

contextUse advice

Only when cancelation is neededcontext

Context has two main functions, cancelation and context.value. Do not use context if you only need to pass values between goroutines. Because in the Go world, context is usually cancelable by default, a context that is not cancelable can easily be misunderstood by callers.

A context that cannot be cancelled has no soul.

context.ValueNot if you can

Access to the context.value content should be the responsibility of the library consumer. Do not use context.Value if it is a flow of data within the library itself, as this part of the data is usually fixed and controllable. Suppose that the authentication module in a certain system requires a string token for authentication. Comparing the following two implementations, it is clear that the token is passed as a parameter.

Context func IsAdminUser(CTX context.context) bool {x := token.getToken (CTX) userObject := auth.AuthenticateToken(x)returnUserObject. IsAdmin () | | userObject. IsRoot ()} / / without the context func IsAdminUser (token string, authService AuthService) int { userObject := authService.AuthenticateToken(token)return userObject.IsAdmin() || userObject.IsRoot()
}
Copy the code

How to correctly use context.Context in Go 1.7

So forget “request-scoped” and think of context.Value as “user-scoped” — let the user, the library caller, decide what to put inside context.Value.

useNewContextandFromContextTo accesscontext

Instead of using context.withvalue () and context.value (“key”) to access data, encapsulating context.value can reduce code redundancy, improve code readability, and minimize careless errors. The comments in the context. context interface provide a good example:

package user

import "context"

// User is the type of value stored in the Contexts.
typeUser struct {... } // key is an unexportedtype for keys defined in this package.
// This prevents collisions with keys defined in other packages.
type key int

// userKey is the key for user.User values in Contexts. It is
// unexported; clients use user.NewContext and user.FromContext
// instead of using this key directly.
var userKey key

// NewContext returns a new Context that carries value u.
func NewContext(ctx context.Context, u *User) context.Context {
	return context.WithValue(ctx, userKey, u)
}

// FromContext returns the User value stored in ctx, if any.
func FromContext(ctx context.Context) (*User, bool) {
	u, ok := ctx.Value(userKey).(*User)
	return u, ok
}
Copy the code

If you are usingcontext.ValuePlease note clearly

As mentioned above, context.value is very unreadable, so we have to compensate with documentation and comments. List at least all possible context. values and their get set methods (NewContext(),FromContext()), and list as many functions as possible that participate in the context.

Encapsulate to reducecontext.TODO()orcontext.Background()

Methods that provide a context but are not used by us as callers still have to pass context.todo () or context.background (). If you can’t stand a lot of useless contexts proliferating in your code, you can encapsulate these methods:

// Suppose there is the following query method, Context func QueryContext(CTX context.context, Query String, args []NamedValue) (Rows, error) {... Query(args []NamedValue) (Rows, error) {return QueryContext(context.Background(), query, args)
}
Copy the code

Other reference

  • How to correctly use context.Context in Go 1.7
  • Understanding the context package in golang
  • Context should go away for Go 2
  • Go Concurrency Patterns: Context