Go Package context: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
Line 66: Line 66:
This section documents the idiomatic pattern to [[#Preemption|preempt]], or [[#Cancellation|cancel]], a blocking function.
This section documents the idiomatic pattern to [[#Preemption|preempt]], or [[#Cancellation|cancel]], a blocking function.


It consists in a <code>someFunc()</code> function that gets a Context instance as its first argument. The function will <code>select</code> reading the "done" channel returned by the Context's <code>Done()</code> method. When the context is [[#Cancelling_Context|externally cancelled]], the "done" channel is closed, reading on the "done" channel unblocks and allows the function to return. The recommended return value in this case is the error returned by <code>[[#Err()|ctx.Err()]]</code>.  
It consists in a <code>someFunc()</code> function that gets a Context instance as its first argument. The function will <code>select</code> reading the Done channel returned by the Context's <code>Done()</code> method. When the context is [[#Cancelling_Context|externally cancelled]], the Done channel is closed, reading operation on the Done channel unblocks and allows the function to return. The recommended return value in this case is the error returned by <code>[[#Err()|ctx.Err()]]</code>.  
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
// someFunc will return a context.Canceled error if the context was externally canceled
// someFunc will return a context.Canceled error if the context was externally canceled
Line 80: Line 80:
</syntaxhighlight>
</syntaxhighlight>


<span id='Cancelling_Context'></span>To '''externally cancel''' the context, and implicitly all functions listening on its "done" channel, invoke the [[#Cancel_Function|cancel function]] returned by the <code>context.WithCancel()</code> function:
<span id='Cancelling_Context'></span>To '''externally cancel''' the context, and implicitly all functions listening on its Done channel, invoke the [[#Cancel_Function|cancel function]] returned by the <code>context.WithCancel()</code> function:
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(context.Background())

Revision as of 17:32, 9 February 2024

External

Internal

Overview

The context package provides two, somewhat unrelated, features.

One is the capability to preempt, or cancel, blocking code executed by goroutines running downstream in the call graph. Cancellation can be programmatic, on timeout or on deadline. This is an idiomatic preemption pattern, equivalent, but preferred to the done channel pattern. It is preferred because its implementation is available in the standard library, and also it is more expressive.

The second feature is the capability to store request-scoped data.

Concepts

Context

https://pkg.go.dev/context#Context

context.Context is an interface exposed by the context package. The Context instances flow across API boundaries and through the system in the same way "done" channels do. The role of the Context instances is to carry cancellation signals, deadlines and request-scoped values. To implement the pattern, each function downstream from the top level concurrent call must take a Context as parameter. Conventionally, is the first parameter of the function. For more rules of using Contexts, see Programming Model. The implementations must ensure that they are concurrent-safe, so their methods may be called by multiple goroutines simultaneously.

type Context interface {
	// Deadline returns the time when work done on behalf of this context should be canceled. 
	Deadline() (deadline time.Time, ok bool)

	// Done returns a channel that's closed when work done on behalf of this context should be canceled. 
	Done() <-chan struct{}

	// 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.
	Err() error

	// Value returns the value associated with this context for key, or nil if no value is associated with key. 
	Value(key any) any
}

Err()

ctx.Err() returns a standard response, as follows:

  • nil is the Done channel is not closed yet.
  • the context.Canceled error, if the context was canceled.
  • the context.DeadlineExceeded error, if the context's deadline has passed.

The Background Context

https://pkg.go.dev/context#Background

The background Context is a non-null, empty, no-deadline and no-values Context returned by the context.Background() function. This context is never canceled, and it is typically used by the main function, initialization and tests, and as the top-level Context for incoming requests.

Derived Context and the Context Hierarchy

Context hierarchy.

Preemption (Cancellation)

Preemption, or cancellation, of a function executing a blocking operation on a channel, consists in the immediate cancellation of the blocking operation and usually returning from the function with a cancellation error.

Cancellation Signal

Cancel Function

Deadline

Timeout

Call Graph

Request

Request-Scoped Data

Programming Model

Rules.

  • To implement the pattern, each function downstream from the top level concurrent call must take a Context as parameter. Conventionally, is the first parameter of the function.

Programmatic Preemption (Cancellation)

This section documents the idiomatic pattern to preempt, or cancel, a blocking function.

It consists in a someFunc() function that gets a Context instance as its first argument. The function will select reading the Done channel returned by the Context's Done() method. When the context is externally cancelled, the Done channel is closed, reading operation on the Done channel unblocks and allows the function to return. The recommended return value in this case is the error returned by ctx.Err().

// someFunc will return a context.Canceled error if the context was externally canceled
func someFunc(ctx context.Context, c <-chan interface{}) error {
	for {
		select {
		case <-ctx.Done():
			return ctx.Err() // context.Canceled is the context was externally canceled 
		case <-c: // this will ensure the goroutine will block, as we will never write on that channel
		}
	}
}

To externally cancel the context, and implicitly all functions listening on its Done channel, invoke the cancel function returned by the context.WithCancel() function:

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

// spin off someFunc() on its own thread and get it to block by reading on the channel we will never write on
go func() {
	err := someFunc(ctx, make(chan interface{}))
	fmt.Printf("someFunc() errored out because %v\n", err)
}()

// spin off the anonymous function that will cancel someFunc() after 5 seconds
go func() {
	time.Sleep(5 * time.Second)
	cancel()
}()

TODO



One of the key differences between Go and other language is explicit context propagation. Context propagation is a mechanism of propagating an additional call argument, called context, into function calls. There is actually a type called context.Context.

The context is used for:

  • Cancellation logic. You can pass a special instance of a context that can get canceled. In that case, all functions you were to call with it would be able to detect this. Can be useful for handling application shutdown or stopping any processing.
  • Timeouts. Timeouts can be set for execution, by using the corresponding context functions.
  • Propagating extra metadata. Additional key/value pairs can be propagate inside the context. Useful in distributed tracing. This feature should be used sparingly, in exceptional cases.

The context is immutable, but it can be cloned with extra metadata.

Function using context should accept it as their first argument.


Manage goroutines lifecycles.

If you have a component that blocks for any reason (disk, network IO, user), then it should probably take the context as a first parameter.

type reportStore interface {
  listTimes(ctx context.Context, ...) (..., error)
  writeFile(ctx context.Context, ...) error
  serveFile(ctx context.Context, ...) error
}


Using the value propagation feature of the Context is dubious. Don't use Context.value for stuff that should be regular dependencies between components. Use Context.value for data that can be passed to your program in any other way: only data that is request-scoped, stuff that is created at the beginning of a request lifecycle, like request ID, etc. If the information is available when the program starts or at any point prior to when the request starts, do not use context.value. This is the case for database handles, loggers, etc.