Goroutines are a big part of the Go language, and we often need to do all kinds of coordination and communication between multiple goroutine.

goroutine

So Go has a particularly unique thing, and it’s the context. You’ll see him at the first argument of various functions, and it’s already a standard.

Scenarios include but are not limited to.

  • Relying on context to pass public contextual information.
  • Asynchronous operations when using goroutine, relying on context to cancel or return errors, etc.
  • Relying on context for cross-Goroutines management and control.

Background

There are many different APIs for the standard library Context, today’s star is the Cancel behavior.

go context cancel

The API call in the code, as follows.

1
ctx, fn := context.WithCancel(ctx)

Combined with the use cases, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func operation1(ctx context.Context) error {
 time.Sleep(100 * time.Millisecond)
 return errors.New("failed")
}

func operation2(ctx context.Context) {
 select {
 case <-time.After(500 * time.Millisecond):
  fmt.Println("done")
 case <-ctx.Done():
  fmt.Println("halted operation2")
 }
}

func main() {
 ctx := context.Background()
 ctx, cancel := context.WithCancel(ctx)

 go func() {
  err := operation1(ctx)
  if err != nil {
   cancel()
  }
 }()

 operation2(ctx)
}

In the above program, when the function operation1 is executed, assuming an error is returned, the context.cancel method is executed, ending the operation2 function that is blocking.

In this way, it’s a perfectly normal Go program. But there is a problem here, that is, after you cancel the context, you only know that it was canceled. What is the reason?

Why was it cancelled, nobody knows…?

It’s a pain in the ass. My friend in the company often see this kind of case, and finally we can only go through the logs or guess the logic according to the clues.

It is rather unreasonable.

New proposal

Similar issues have been proposed before, that is, to “facilitate debugging where contexts are cancelled”, that is, to address the handling of cancelled scenarios.

After several years of discussion, @Sameer Ajmani came up with a new proposal “proposal: context: add APIs for writing and reading cancelation cause” to solve this problem.

Several new APIs will be added as follows.

1
2
3
4
5
6
7
8
9
package context

type CancelCauseFunc func(cause error)

func Cause(c Context) error

func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc)

func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)

Use cases.

1
2
3
4
5
ctx, cancel := context.WithCancelCause(parent)
cancel(myError)

ctx.Err() // returns context.Canceled
context.Cause(ctx) // returns myError

After calling the WithCancelCause or WithTimeoutCause methods, a CancelCauseFunc is returned instead of a CancelFunc.

The difference is that you can get the root cause of the cancelled error by passing in the corresponding Error, etc., and then calling the Cause method.

That is, you can get both the status of the canceled (context.Canceled) and the corresponding error message (myError) to solve the scenario mentioned in the previous article.

Summary

In this article, we introduced a missing design scenario for Go’s most common standard library, Context. It is not enough to have a Context status code; in business design, status codes and error messages should be matched.

When this proposal is merged, I believe developers who have had similar experiences in the past can reduce some troubleshooting time.