The official blog on 24 February 2021 details some best practices for using context, provides code examples of why context should not be stored inside a struct and is best passed as the first argument to a function, And how to store the context in a struct in the safest way possible if necessary (to maintain backward compatibility). The following is the main content of the original text.

Introduction

In many Go apis, especially modern ones, the first argument to functions and methods is usually context.context. Context provides methods such as WithCancel, WithDeadline, and WithValue to implement flow control across apis. Many LiBs often use context for control when interacting with remote servers (such as databases, apis, etc.).

The context documentation says:

Context should not be stored instructInternally, the best way is to pass it as the first argument to the functionCopy the code

This article builds on this advice with reasons and examples for why it is important to use functions to pass the Context instead of storing it in a struct. It also uses net/ HTTP code as an example to explain when you can store a Context in a struct.

It is recommended that context be passed as a function parameter

Let’s first look at passing the context in a function:

type Worker struct { / *... * / }

type Work struct { / *... * / }

func New(a) *Worker {
  return &Worker{}
}

func (w *Worker) Fetch(ctx context.Context) (*Work, error) {
  _ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}

func (w *Worker) Process(ctx context.Context, w *Work) error {
  _ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}
Copy the code

Here (*Worker).fetch and (*Worker).process both take context directly as the first argument to the function. In this way, from the generation of the context to the end, the caller can clearly see the context’s path.

There is some confusion about storing the context in strcut

To implement the Worker example above by nesting a context in a structure, the caller is confused about the context lifecycle being used:

type Worker struct {
  ctx context.Context
}

func New(ctx context.Context) *Worker {
  return &Worker{ctx: ctx}
}

func (w *Worker) Fetch(a) (*Work, error) {
  _ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}

func (w *Worker) Process(w *Work) error {
  _ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}
Copy the code

The (*Worker).fetch and (*Worker).process methods both use the context in the Worker structure, which makes it impossible for the caller to define a different context, such as WitchCancel, Some people want to use WithDeadline, but it’s hard to understand that the context is cancel, right? Or the deadline? The lifecycle of the context used by the caller is bound to a shared context.

Special case: Retain backward compatibility

When go 1.7 was released, a number of apis were required to support context. context in a backward compatible manner, for example, the Client methods of NET/HTTP (such as Get and Do) were the model for using context. Any HTTP request sent using these methods will benefit from the WithDeadline, WithCancel, and WIthValue methods that accompany context. context.

There are generally two ways to support context. context while keeping your code backward compatible:

  1. Add the context to the struct (we’ll see later);
  2. Copy the original function, use context in the first argument, for example,database/sqlThis packageQueryThe method’s signature is always:
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
Copy the code

When the Context Package was introduced, the Go Team added a function like this:

func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)
Copy the code

And only one code change:

func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
    return db.QueryContext(context.Background(), query, args...)
}
Copy the code

In this way, Go Team can smoothly upgrade a package without compromising code readability or compatibility. Similar code can be found everywhere in the Golang source code. For more on keeping code compatible, see [the Go Team’s practices on keeping Go Modules compliant].

However, in some cases, such as when your API exposes a large number of functions, it may not be practical to rewrite all functions.

The package NET/HTTP option to add context. context to a struct is a more appropriate example of a struct nesting context. Let’s first look at the NET/HTTP Do function. Before introducing context, Do is defined as follows:

func (c *Client) Do(req *Request) (*Response, error)
Copy the code

In 1.7, context is introduced. Maintainers add context. context in the http.Request structure, in order to maintain the backward compatibility principle of the NET/HTTP standard library and consider that the core library contains too many functions.

type Request struct {
  ctx context.Context

  // ...
}

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
  // Simplified for brevity of this article.
  return &Request{
    ctx: ctx,
    // ...}}func (c *Client) Do(req *Request) (*Response, error)
Copy the code

When modifying a large number of apis to support context, it makes sense to add context.context to the structure, as described above. However, remember to consider copying functions to support context first, to be backward compatible with context without sacrificing utility or understanding:

func (c *Client) Call(a) error {
  return c.CallContext(context.Background())
}

func (c *Client) CallContext(ctx context.Context) error {
  // ...
}
Copy the code

Conclusion

Context makes it easy to propagate important cross-lib and cross-API information in the call stack. However, it must be used consistently and clearly to make it easy to understand, easy to debug, and effective.

When context is passed as the first parameter in a method rather than stored in a struct, the user can take full advantage of its extensibility by building a WithCancel, WithDeadline, and WithValue transfer tree from the call stack. And, most importantly, when passed in as a parameter, you have a clear idea of the extent of its propagation, making it easy to understand and debug.

When designing an API with context, remember the advice: pass context.Context in as an argument; don’t store it in structs.