1. Context Common methods and what scenarios are they applicable to
1.1 Methods contained in context
var ctx context.Context
var cancel context.CancelFunc
// 1, pass the value of key, value
ctx = context.WithValue(context.Background(), "key"."value")
// 2
ctx, cancel = context.WithTimeout(context.Background(), time.Second*10)
// 3, timeout control, deadline
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Second*10))
// 4, cancel control
ctx, cancel = context.WithCancel(context.Background())
// 5, as a good habit, defer fires the cancel function; Ps: Cancel function idempotent
defer cancel()
Copy the code
1.2 Application scenarios and pseudocode examples
1.2.1 Value Transfer: For example, the GIN framework is used to transfer key and value values. A simple example is as follows
func readContext(ctx context.Context) {
traceId, ok := ctx.Value("key"). (string)
if ok {
fmt.Printf("readContext key=%s\n", traceId)
} else {
fmt.Printf("readContext no key\n")}}func main(a) {
ctx := context.Background()
readContext(ctx) //output: readContext no key
ctx = context.WithValue(ctx, "key"."beautiful")
readContext(ctx) //output: readContext key=beautiful
}
func TestWithValueContext(t *testing.T) {
main()
}
Copy the code
1.2.2 Timeout Control-timeout: specifies the timeout period for an HTTP request
func httpRequest(ctx context.Context) {
for {
// process http requests
select {
case <-ctx.Done():
fmt.Println("http requests cancel")
return
case <-time.After(time.Second * 1) :}}}func TestTimeoutContext(t *testing.T) {
fmt.Println("start TestTimeoutContext")
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
httpRequest(ctx)
time.Sleep(time.Second * 5)}Copy the code
1.2.3. Time-out control-deadline: For time-consuming operations such as file I/O or network I/O, check whether the remaining time is sufficient and decide whether to proceed to the next operation
func copyFile(ctx context.Context) {
deadline, ok := ctx.Deadline()
if ok == false {
return
}
isEnough := deadline.Sub(time.Now()) > time.Second*5
if isEnough {
// write content to file
fmt.Println("copy file")}else {
fmt.Println("isEnough is false return")
return}}func TestDeadlineContext(t *testing.T) {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*4))
defer cancel()
copyFile(ctx)
time.Sleep(time.Second * 5)}Copy the code
1.2.4. Cancel control: Goroutine sends a cancel signal to ensure that all goroutines emanating from its logic are cancelled successfully
func gen(ctx context.Context) <-chan int {
ch := make(chan int)
go func(a) {
var n int
for {
select {
case ch <- n:
n++
time.Sleep(time.Second)
case <-ctx.Done():
return}}} ()return ch
}
func TestCancelContext(t *testing.T) {
// Create a Cancel context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
// Cancel is triggered when the requirement is met
cancel()
break}}}Copy the code
2. What is the underlying implementation of the context package?
2.1 Key, value Low-level implementation of value transmission
- Golang v1.16), the core of which is that when the context cannot obtain the value of the key, the parent context recursively retrieves the value
type valueCtx struct {
Context // Holds the parent of this node
key, val interface{} // Store the key and value values of this object
}
// Create an implementation of the value context
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")}if key == nil {
panic("nil key")}if! reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}
func (c *valueCtx) Value(key interface{}) interface{} {
// If the key and value are present in this object, the value can be set directly
if c.key == key {
return c.val
}
// Otherwise, we recurse to the parent
return c.Context.Value(key)
}
Copy the code
2.2 cancel implementation
CancelCtx structure
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
// Triggering cancellation cancels all child nodes recursively
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
Copy the code
2.2.2 cancelCtx implements the cancel function. The logic is as follows
- 1. Locks ensure and avoid concurrent conflicts
- 2. Close the c. Tone channel and pass the signal through it.
- 3. Iterate over and close all child nodes to ensure no memory leakage
- 4. After releasing all its child nodes, it assigns its child node map to nil
- Remove itself from its parent node. This is only triggered when the WithCancel () method is called, meaning that the removeFromParent argument is true.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock() // 1, the lock guarantees concurrency conflicts
ifc.err ! =nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done) // close the done channel
}
// 3, iterate over all child nodes, and call cancel
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
// 4, after releasing all of its children, assign its children map to nil
c.children = nil
c.mu.Unlock()
// 5, removes itself from its parent, which only passes true when withCancel is called
if removeFromParent {
removeChild(c.Context, c)
}
}
Copy the code
2.2.3 Refinement: signal transmission of C. Tone
- This is based on the property of all channels. When listening on a channel, it will block if the channel is empty, but if the channel is closed, it will not block and will read a null value
- Based on the above features, this channel is closed, and all other Goroutines listening on this channel receive the signal
- The code for
func Done(ch chan struct{}, count int) {
for {
select {
case <-ch:
fmt.Println(count)
return}}}func TestCloseChannel(t *testing.T) {
signalChannel := make(chan struct{})
go Done(signalChannel, 1)
go Done(signalChannel, 2)
go Done(signalChannel, 3)
time.Sleep(3)
fmt.Println("close signalChannel")
close(signalChannel)
// Block the current goroutine
select{}}Copy the code
2.2.4 Refinement: The removeFromParent parameter – whether to delete itself from the parent node
- Take a look at the implementation code for removeChild
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if! ok {return
}
p.mu.Lock()
ifp.children ! =nil {
delete(p.children, child)
}
p.mu.Unlock()
}
Copy the code
- Why is removeFromParent=true and removeChild () called when WithCancel () is called when a new node is created?
- Because more of the processing that you’re calling cancel is tied to the child nodes in your context, only the last step really frees itself
- For example:
- 1, Step 1: If you create a cancelContext that is attached to the root node, then all children below you will be released because of your C. child = nil.
- Step 2: Then logically you have called cancel yourself, so you have to release yourself, so delete yourself from the parent node
- Why is it not called when other child nodes are deleted?
- 1, because one of the operations is delete(p.child), this operation deletes itself from the map of the children of the parent node, and traversing and deleting the map at the same time is problematic
- 2, and since there is an operation c. dren = nil in cancel (), there is no need to do it