Understand the Go language Context in one article

This article has participated in the “Newcomer Creation Ceremony” activity, and together we will start the road to gold nugget creation.

0 pre-knowledge sync.WaitGroup

sync.WaitGroup is to wait for a group of coroutines to end. It implements a structure similar to a task queue. Tasks can be added to the queue, and tasks will be removed from the queue after the tasks are completed. If the tasks in the queue are not all completed, the queue will trigger blocking to prevent the program from continuing to run.
sync.WaitGroup has only 3 methods, Add(), Done(), Wait().

Among them, Done() is an alias of Add(-1), use Add() to add a count, Done() subtracts a count, if the count is not 0, it blocks the operation of Wait().

Example:

package main

import (
   "fmt"
   "sync"
   "time"
)

var group sync.WaitGroup

func sayHello() {<!-- -->
   for i := 0; i < 5; i + + {<!-- -->
      fmt.Println("hello......")
      time. Sleep(time. Second)
   }
   //Thread end -1
   group. Done()
}

func sayHi() {<!-- -->
   //Thread end -1
   defer group. Done()
   for i := 0; i < 5; i + + {<!-- -->
      fmt.Println("hi......")
      time. Sleep(time. Second)
   }
}

func main() {<!-- -->
   //+2
   group. Add(2)
   fmt.Println("main is blocking...")
   go sayHello()
   fmt.Println("main keeps blocking...")
   go sayHi()
   // thread waiting
   group. Wait()
   fmt.Println("main seems to be blocked...")
}

Effect:

1 Introduction

In a Go server, each incoming request is handled in its own goroutine. Request handlers typically start additional goroutines to access backends such as databases and RPC services. A set of goroutines processing a request often needs access to request-specific values such as the end user’s identity, authorization token, and the request’s expiration date. When a request is canceled or times out, all goroutines handling that request should exit quickly so that the system can reclaim any resources they were using.

To this end, a context package was developed that makes it easy to pass request-scoped values, cancellation signals, and deadlines across API boundaries to all goroutines involved in processing a request.

Context carries a deadline, a cancellation signal, and other values that cross API boundaries. The context’s methods can be called by multiple gor routines at the same time.

Incoming requests to the server should create a context, and outgoing calls to the server should accept a context. The chain of function calls between them must propagate the Context, optionally replacing it with a derived Context created using WithCancel, WithDeadline, WithTimeout, or WithValue. When a context is canceled, all contexts derived from it are also canceled.

The WithCancel, WithDeadline, and WithTimeout functions take a Context (parent) and return a derived Context (child) and CancelFunc. Calling CancelFunc cancels the child and its children, removing the parent’s reference to the child, and stopping any associated timers. Failure to call CancelFunc leaks the child and its children until the parent is canceled or the timer fires. The go vet tool checks that CancelFuncs are used on all control flow paths.

Programs using context should follow the following rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:

Don’t store the context in a struct type; instead, pass the Context explicitly to every function that needs it. Context should be the first parameter, usually named ctx:

func DoSomething(ctx context.Context, arg Arg) error {<!-- -->
// ... use ctx ...
}

Do not pass a nil context, even if the function allows it. If you are not sure which Context to use, pass context.TODO.

Use context values only for request-scoped data for transfer processes and APIs, not for passing optional parameters to functions.

The same Context can be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

2 context.Context introduction

//The context carries deadlines, cancellation signals, and request-scoped values at the boundaries of the API. Its method is safe to use multiple goroutines at the same time.
type Context interface {<!-- -->
    // Done returns a channel that is closed when the context is canceled or timed out.
    Done() <-chan struct{<!-- -->}

    // Err indicates why this context was canceled after the Done channel was closed.
    Err() error

    // Deadline returns the time (if any) when the context will be canceled.
    Deadline() (deadline time. Time, ok bool)

    // Value returns the value associated with the key, or nil if there is none.
    Value(key interface{<!-- -->}) interface{<!-- -->}
}
  • The Done method returns a channel that acts as a cancellation signal on behalf of the function running the Context: when the channel is closed, the functions should abandon their work and return.
  • The Err method returns an error indicating the reason for the Context cancellation.
  • A Context is safe for multiple goroutines to use concurrently. Code can pass a single Context to any number of goroutines and cancel its Context to signal all goroutines.
  • The Deadline method allows functions to determine whether they should start work, and can also use deadlines to set timeouts for I/O operations.
  • Value allows a Context to carry request-scoped data. This data must be safe for concurrent use by multiple goroutines.

Other common functions of the 3 context package

3.1 context.Background and context.TODO

Background is the root of any Context tree, it is never canceled:

//Background returns an empty Context. It's never canceled, has no expiration date, and has no value. Background is typically used in main, init, and tests, and acts as the top-level context for incoming requests.
func Background() Context

When passing Context to a function method, don’t pass nil. If you don’t know what to pass, use context.TODO()

3.2 context.WithCancel and

WithCancelt returns a derived Context value that can be canceled faster than the parent Context. When the request handler returns, it usually cancels the content associated with the incoming request. WithCancel is also useful for canceling redundant requests when using multiple replicas.

// WithCancel returns a copy of the parent process whose Done channel is closed as soon as possible. Close Done or call cancel.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// CancelFunc cancels a context.
type CancelFunc func()

Example:

package main

import (
   "context"
   "fmt"
)

func play(ctx context.Context) <-chan int {<!-- -->
   dist := make(chan int)
   n := 1
   //Anonymous function adds elements to dist
   go func() {<!-- -->
      for {<!-- -->
         select {<!-- -->
         //This will not be executed when ctx is empty
         case <- ctx. Done():
            return // return ends the goroutine to prevent leaks
            // add elements to dist
         case dist <- n:
            n + +
         }
      }
   }()
   return dist
}
func main() {<!-- -->
   // return empty context
   ctx, cancel := context.WithCancel(context.Background())
   defer cancel() // call cancel
   for n := range play(ctx) {<!-- -->
      fmt. Println(n)
      if n == 5 {<!-- -->
         break
      }
   }
}

Extension: usage of select in go

The usage of select is very similar to the switch language. Select starts a new selection block, and each selection condition is described by a case statement.
Compared with the switch statement, select has more restrictions. The biggest limitation is that each case statement must be an IO operation. The general structure is as follows:
```go
select {
   case <- chan1:
      // If chan1 successfully reads the data, execute the case processing statement
   case chan2 <- 1:
      // If the data is successfully written to chan2, execute the case processing statement
   default:
      // If none of the above is successful, enter the default processing flow
}

In a select statement, the Go language evaluates each send and receive statement sequentially from start to finish.
If any of the statements can continue to execute (that is, not blocked), then choose any one of those executable statements to use.
If none of the statements can be executed (i.e. all channels are blocked), then there are two possible cases:

  • If the default statement is given, then the default statement will be executed, and program execution will resume from the statement following the select statement.
  • If there is no default statement, then the select statement will block until at least one communication can proceed.
#### 3.3 context. WithTimeout

WithTimeout returns the derived Context value, and WithTimeout is used to set the deadline for requests to the backend server:

```go
//WithTimeout returns a copy of the parent process whose Done channel is closed immediately. Closing 'done', calling 'cancel', or a timeout elapses. new
// Context's Deadline is now faster + timeout and parent's Deadline, if any. If the timer is still running, the cancel function releases its resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// CancelFunc cancels a context.
type CancelFunc func()

Example:

package main

import (
   "context"
   "fmt"
   "sync"
   "time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {<!-- -->
    LOOP:
   for {<!-- -->
      fmt.Println("db connecting...")
      time.Sleep(time.Millisecond * 10) // Assume that it takes 10 milliseconds to connect to the database normally
      select {<!-- -->
      case <-ctx.Done(): // automatically called after 50 milliseconds
         break LOOP
      default:
      }
   }
   fmt.Println("worker done!")
   wg. Done()
}

func main() {<!-- -->
   // Set a timeout of 50 milliseconds
   ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
   wg. Add(1)
   go worker(ctx)
   time. Sleep(time. Second * 5)
   cancel() // notify the child goroutine to end
   wg. Wait()
   fmt.Println("over")
}

Results of the:

3.4 context. WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {<!-- -->
   if parent == nil {<!-- -->
      panic("cannot create context from nil parent")
   }
   if cur, ok := parent.Deadline(); ok & amp; & amp; cur.Before(d) {<!-- -->
      // The current deadline is already ahead of the new deadline
      return WithCancel(parent)
   }
   c := &timerCtx{<!-- -->
      cancelCtx: newCancelCtx(parent),
      deadline: d,
   }
   propagateCancel(parent, c)
   dur := time.Until(d)
   if dur <= 0 {<!-- -->
      c.cancel(true, DeadlineExceeded) // Deadline has passed
      return c, func() {<!-- --> c. cancel(false, Canceled) }
   }
   c.mu.Lock()
   defer c.mu.Unlock()
   if c.err == nil {<!-- -->
      c.timer = time.AfterFunc(dur, func() {<!-- -->
         c. cancel(true, DeadlineExceeded)
      })
   }
   return c, func() {<!-- --> c. cancel(true, Canceled) }
}

Example:

package main

import (
   "context"
   "fmt"
   "time"
)

func main() {<!-- -->
   d := time.Now().Add(500 * time.Millisecond)
   ctx, cancel := context.WithDeadline(context.Background(), d)
   // Although ctx will expire, it is good practice to call its cancel function in any case.
   // Failure to do so may keep the context and its parent classes alive longer than necessary.
   defer cancel()
   select {<!-- -->
   case <- time.After(1 * time.Second):
      fmt.Println("over")
   case <- ctx. Done():
      fmt.Println(ctx.Err())
   }
}

Results of the:

3.5 context. WithValue

WithValue provides a way to associate a request-scoped value with a Context ?

//WithValue returns a copy of the parent element, and its Value method returns val for key.
func WithValue(parent Context, key interface{<!-- -->}, val interface{<!-- -->}) Context

The best way to learn how to use the context package is through a working example.

Example:

package main

import (
   "context"
   "fmt"
   "sync"
   "time"
)

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {<!-- -->
   key := TraceCode("KEY_CODE")
   traceCode, ok := ctx.Value(key).(string) // get trace code in child goroutine
   if !ok {<!-- -->
      fmt.Println("invalid trace code")
   }
    LOOP:
   for {<!-- -->
      fmt.Printf("worker, code:%s\\
", traceCode)
      time.Sleep(time.Millisecond * 10) // Assume that it takes 10 milliseconds to connect to the database normally
      select {<!-- -->
      case <-ctx.Done(): // automatically called after 50 milliseconds
         break LOOP
      default:
      }
   }
   fmt.Println("worker is over!")
   wg. Done()
}

func main() {<!-- -->
   // Set a timeout of 50 milliseconds
   ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
   // Set the trace code at the entry of the system and pass it to the subsequently started goroutine to achieve log data aggregation
   ctx = context.WithValue(ctx, TraceCode("KEY_CODE"), "12512312234")
   wg. Add(1)
   go worker(ctx)
   time. Sleep(time. Second * 5)
   cancel() // notify the child goroutine to end
   wg. Wait()
   fmt.Println("over")
}

Results of the:

Example 4: request browser timeout

server side

package main

import (
   "fmt"
   "math/rand"
   "net/http"
   "time"
)

// On the server side, slow response occurs randomly
func indexHandler(w http.ResponseWriter, r *http.Request) {<!-- -->
   number := rand.Intn(2)
   if number == 0 {<!-- -->
      time.Sleep(time.Second * 10) // Slow response that takes 10 seconds
      fmt.Fprintf(w, "slow response")
      return
   }
   fmt.Fprint(w, "quick response")
}

func main() {<!-- -->
   http.HandleFunc("/", indexHandler)
   err := http.ListenAndServe(":9999", nil)
   if err != nil {<!-- -->
      panic(err)
   }
}

client side

package main

import (
   "context"
   "fmt"
   "io/ioutil"
   "net/http"
   "sync"
   "time"
)

// client

type respData struct {<!-- -->
   resp *http.Response
   err error
}

func doCall(ctx context.Context) {<!-- -->
   // http long connection
   transport := http.Transport{<!-- -->DisableKeepAlives: true}
   client := http.Client{<!-- -->Transport: &transport}

   respChan := make(chan *respData, 1)
   req, err := http.NewRequest("GET", "http://127.0.0.1:9999/", nil)
   if err != nil {<!-- -->
      fmt.Println(err)
      return
   }
   req = req.WithContext(ctx) // Create a new client request with ctx with timeout
   var wg sync.WaitGroup
   wg. Add(1)
   defer wg. Wait()
   go func() {<!-- -->
      resp, err := client. Do(req)
      fmt.Printf("resp:%v, err:%v\\
", resp, err)
      rd := &respData{<!-- -->
         resp: resp,
         err: err,
      }
      respChan <- rd
      wg. Done()
   }()

   select {<!-- -->
   case <- ctx. Done():
      fmt.Println("timeout...")
   case result := <-respChan:
      fmt.Println("success....")
      if result.err != nil {<!-- -->
         fmt.Printf("err:%v\\
", result.err)
         return
      }
      defer result.resp.Body.Close()
      data, _ := ioutil. ReadAll(result. resp. Body)
      fmt.Printf("resp:%v\\
", string(data))
   }
}

func main() {<!-- -->
   // define a timeout of 100 milliseconds
   ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
   defer cancel() // call cancel to release child goroutine resources
   doCall(ctx)
}

Where are the 5 Context packages used

Many server frameworks provide packages and types for carrying request-scoped values. We can define new implementations of the “Context” interface, bridging the gap between code that uses the existing framework and code that expects a “Context” parameter.

6 Summary

At Google, Go programmers are required to pass a “Context” parameter as the first argument to every function on the call path between incoming and outgoing requests. This allows Go code developed by many different teams to interoperate well. It provides easy control over timeouts and cancellations, and ensures that key values like security credentials are properly transferred to Go programs.

Server frameworks that want to build on “Context” should provide implementations of “Context” to bridge their packages with those that expect a “Context” parameter. Their client libraries will then accept the “Context” from the calling code. By establishing a common interface for request-scoped data and cancellation, “context” makes it easier for package developers to share code that creates scalable services.

Reference article:

golang.google.cn/pkg/context…

go.dev/blog/contex…