SoFunction
Updated on 2025-04-09

The implementation principle of Context in Go and the correct way to use it

1. Basic Principles

1.1 Introduction to Context package

In Go, the Context package is a mechanism for passing request range data, cancel signals, and deadlines. It is often used to handle communication and cancellation between goroutines. The Context package is built in Go and it can be used easily without additional dependencies.
The Context package is a lightweight tool that provides a standard interface for passing request-range data, cancel signals, and deadlines between goroutines. The Context package implements a tree-like data structure, where each node represents a request range. Each node has a unique key-value pair, where key is a value of type interface{} and value is a value of any type. The Context package also provides an optional timeout mechanism for automatically canceling requests after a certain period of time.

The core of the Context package is a Context interface that defines methods to obtain request scoped data, cancel requests, and handle timeouts.

 type Context interface {
     Deadline() (deadline , ok bool)
     Done() <-chan struct{}
     Err() error
     Value(key interface{}) interface{}
 }
  • The Deadline() method returns the deadline and a Boolean value indicating whether the deadline has been set.
  • The Done() method returns a read-only channel that will be closed when the request is cancelled or timed out.
  • The Err() method returns an error indicating why the request was cancelled.
  • The Value() method returns the value associated with the given key, and if there is no value, returns nil.

The Context package also provides two functions for creating Context: WithContext and Background. The Background function returns an empty Context, while the WithContext function creates a new Context based on the given parent Context.
The basic principle of the Context package is to manage request scope data, cancel signals and deadlines by passing Context between goroutines. When a goroutine creates a new goroutine, it passes the Context as a parameter to the new goroutine. The new goroutine can use this Context to access request range data, receive cancel signals, and process timeouts.

1.2 Context creation

In Golang, Context can be created through functions such as WithCancel, WithDeadline, WithTimeout, and WithValue. The usage and precautions of these functions are introduced below.

1.2.1 WithCancel

The WithCancel function can be used to create a Context object and return a cancelable context and a cancel function. When a cancel function is called, all Context objects and their children Context objects are notified, causing them to be canceled.

 func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

Here is a sample code:

package main
 ​
 import (
     "context"
     "fmt"
     "time"
 )
 ​
 func main() {
     parent := ()
     ctx, cancel := (parent)
     go func() {
         select {
         case <-():
             (())
             return
         case <-(5 * ):
             ("work done")
         }
     }()
     (10 * )
     cancel()
     (1 * )
 }

In the above code, we first create a root Context object parent using the () function, and then create a child Context object ctx using the WithCancel function, and return a cancelable context and a cancel function cancel. Next, we use the select statement in a goroutine to listen to the Done method of the Context object and the return value of the function. If the Done method returns a non-nil error, it means that the Context has been cancelled, otherwise it means that the function has timed out. In the main function, we call the cancel function to notify the Context object and its children Context objects, so that they both cancel execution. Finally, we use the function to make the program wait for a while in order to observe the execution of the Context.

1.2.2 WithDeadline

The WithDeadline function can be used to create a Context object and return a deadline and a cancel function. When the deadline is exceeded, all Context objects and their children Context objects are automatically notified to cancel execution.

func WithDeadline(parent Context, deadline ) (ctx Context, cancel CancelFunc)

Here is a sample code:

package main
 ​
 import (
     "context"
     "fmt"
     "time"
 )
 ​
 func main() {
     parent := ()
     ctx, cancel := (parent, ().Add(5*))
     go func() {
         select {
         case <-():
             (())
             return
         case <-(10 * ):
             ("work done")
         }
     }()
     (20 * )
     cancel()
     (1 * )
 }

In the above code, we first create a root Context object parent using the () function, and then create a child Context object ctx using the WithDeadline function, and return a deadline and a cancel function cancel. Next, we use the select statement in a goroutine to listen to the Done method of the Context object and the return value of the function. If the Done method returns a non-nil error, it means that the Context has been cancelled, otherwise it means that the function has timed out. In the main function, we call the cancel function to notify the Context object and its children Context objects, so that they both cancel execution. Finally, we use the function to make the program wait for a while in order to observe the execution of the Context.

1.2.3 WithTimeout

The WithTimeout function can be used to create a Context object and return a timeout and a cancel function. When the timeout time is exceeded, all Context objects and their children Context objects are automatically notified to cancel the execution.

func WithTimeout(parent Context, timeout ) (ctx Context, cancel CancelFunc)

Here is a sample code:

package main
 ​
 import (
     "context"
     "fmt"
     "time"
 )
 ​
 func main() {
     parent := ()
     ctx, cancel := (parent, 5*)
     go func() {
         select {
         case <-():
             (())
             return
         case <-(10 * ):
             ("work done")
         }
     }()
     (20 * )
     cancel()
     (1 * )
 }

In the above code, we first create a root Context object parent using the () function, and then create a child Context object ctx using the WithTimeout function, and return a timeout and a cancel function cancel. Next, we use the select statement in a goroutine to listen to the Done method of the Context object and the return value of the function. If the Done method returns a non-nil error, it means that the Context has been cancelled, otherwise it means that the function has timed out. In the main function, we call the cancel function to notify the Context object and its children Context objects, so that they both cancel execution. Finally, we use the function to make the program wait for a while in order to observe the execution of the Context.

1.2.4 WithValue

The WithValue function can be used to create a Context object and return a Context object with the specified value. This value can be any type of data, it can be a basic type, structure, or pointer. It should be noted that this value is only valid in the current Context object and its child Context objects, and is invisible to other Context objects.

func WithValue(parent Context, key interface{}, val interface{}) Context

Here is a sample code:

package main
 ​
 import (
     "context"
     "fmt"
 )
 ​
 type userKey struct{}
 ​
 func main() {
     parent := ()
     ctx := (parent, userKey{}, "admin")
     go func() {
         if user, ok := (userKey{}).(string); ok {
             ("user is %s\n", user)
         } else {
             ("user is not found")
         }
     }()
     select {}
 }

In the above code, we first create a root Context object parent using the () function, and then create a child Context object ctx using the WithValue function, and return a Context object with the specified value. Next, we use the function in a goroutine to get the value in the Context object and determine whether its type is a string type. If yes, output its value, otherwise output "user is not found". In the main function, we use the select statement to keep the program running so that we can observe the execution of the Context.

2. Context usage scenarios

2.1 Concurrent control

A very typical use scenario is that when we need to start multiple goroutines for task processing at the same time, we can use Context to control the execution of these goroutines. In each goroutine, we can detect whether the Context object is cancelled, and if so, the execution of the goroutine is exited, otherwise the execution will continue.

Here is a sample code:

package main
 ​
 import (
     "context"
     "fmt"
     "sync"
 )
 ​
 func worker(ctx , wg *) {
     defer ()
     for {
         select {
         default:
             ("work")
         case <-():
             return
         }
     }
 }
 ​
 func main() {
     parent := ()
     ctx, cancel := (parent)
     var wg 
     for i := 0; i < 3; i++ {
         (1)
         go worker(ctx, &wg)
     }
     cancel()
     ()
 }

In the above code, we first create a root Context object parent using the () function, and then create a child Context object ctx using the WithCancel function, and return a cancel function cancel. Next, we use , to wait for all goroutine execution to complete. In the main function, we start three goroutines to execute the task, and use the cancel function to notify these goroutines to cancel execution. Finally, we use the Wait method to wait for all goroutine execution to complete.

2.2 Timeout control

Another typical use scenario is that when we need to set a timeout time for an operation, we can use Context to control the execution time of the operation. When the operation execution timeout, we can notify the Context object and its children Context object to cancel execution.
Here is a sample code:

package main
 ​
 import (
     "context"
     "fmt"
     "time"
 )
 ​
 func work(ctx ) {
     for {
         select {
         default:
             ("work")
         case <-():
             ("work done")
             return
         }
     }
 }
 
 func main() {
     parent := ()
     ctx, cancel := (parent, *5)
     defer cancel()
     work(ctx)
 }

In the above code, we first create a root Context object parent using the () function, and then create a child Context object ctx using the WithTimeout function, and return a cancel function cancel. In the work function, we start an infinite loop and continuously output "work". At the same time, we use the select statement to wait for the Context object to be cancelled. In the main function, we call the cancel function using the defer statement to ensure that the Context object is cancelled. Since we set a 5-second timeout in the WithTimeout function, the work function stops executing when the program runs for more than 5 seconds.

2.3 Database connection

When using database connections, we usually need to ensure that the number of connections in the connection pool does not exceed a certain threshold. If the number of connections in the connection pool exceeds the threshold, you need to wait for the connection to be released before operating. In this case, we can use Context to control the life cycle of the connection.

Here is a sample code:

package main
 ​
 import (
     "context"
     "database/sql"
     "fmt"
     "sync"
     "time"
 ​
     _ "/go-sql-driver/mysql"
 )
 ​
 const maxConn = 5
 ​
 func main() {
     db, err := ("mysql", "root:root@tcp(127.0.0.1:3306)/test")
     if err != nil {
         panic(err)
     }
     defer ()
 ​
     ctx, cancel := ((), *5)
     defer cancel()
 ​
     connCh := make(chan *, maxConn)
     var wg 
     for i := 0; i < maxConn; i++ {
         (1)
         go func() {
             defer ()
             for {
                 select {
                 case <-():
                     return
                 default:
                     if len(connCh) < maxConn {
                         conn, err := (ctx)
                         if err != nil {
                             (err)
                             return
                         }
                         connCh <- conn
                     }
                 }
             }
         }()
     }
     ()
 }

In the above code, we first use the function to open a connection to a MySQL database and return a DB object db. Next, we use the WithTimeout function to create a Context object ctx and set a timeout of 5 seconds. At the same time, we create a channel object connCh with a capacity of maxConn to store database connections. In g oroutine, we use the select statement to wait for the Context object to be cancelled. In each loop, we check if the number of connections in the connection pool exceeds the threshold, and if not, use the function to get a new connection from the connection pool and store it in connCh. Finally, we use Wait for all goroutine executions to complete.

2.4 HTTP Request

When using HTTP requests, we usually need to set a timeout time to ensure that the request can be responded within the specified time. In this case, we can use Context to control the execution time of HTTP requests.

Here is a sample code:

package main
 ​
 import (
     "context"
     "fmt"
     "io/ioutil"
     "net/http"
     "time"
 )
 ​
 func main() {
     client := 
     ctx, cancel := ((), *5)
     defer cancel()
     req, err := (ctx, , "", nil)
     if err != nil {
         (err)
         return
     }
 ​
     resp, err := (req)
     if err != nil {
         (err)
         return
     }
     defer ()
 ​
     body, err := ()
     if err != nil {
         (err)
         return
     }
 ​
     (string(body))
 }

In the above code, we first create an HTTP client object client using . Next, we use the WithTimeout function to create a Context object ctx and set a timeout of 5 seconds. At the same time, we use the function to create an HTTP request object req and pass the Context object ctx as a parameter to the function. In the Do function, we will automatically pass the Context object ctx to the HTTP request and automatically cancel the request after the timeout time arrives.

2.5 gRPC request

When using gRPC requests, we usually need to set a timeout time to ensure that the request can be responded within the specified time. In this case, we can use Context to control the execution time of gRPC requests.

Here is a sample code:

package main
 ​
 import (
     "context"
     "fmt"
     "log"
     "time"
 ​
     pb "/example/helloworld"
     "/grpc"
 )
 ​
 const (
     address     = "localhost:50051"
     defaultName = "world"
 )
 ​
 func main() {
     conn, err := (address, ())
     if err != nil {
         ("did not connect: %v", err)
     }
     defer ()
 ​
     c := (conn)
 ​
     ctx, cancel := ((), *5)
     defer cancel()
 ​
     r, err := (ctx, &{Name: defaultName})
     if err != nil {
         ("could not greet: %v", err)
     }
     ("Greeting: %s", ())
 }

In the above code, we first use the function to create a gRPC client connection object conn. Next, we use the function to create a GreeterClient object c. Then, we use the WithTimeout function to create a Context object ctx and set a timeout of 5 seconds. Finally, we use the SayHello function of the GreeterClient object c to send a gRPC request and pass the Context object ctx as a parameter to the function. In the SayHello function, we will automatically pass the Context object ctx to the gRPC request and automatically cancel the request after the timeout has arrived.

Go Context: Should I put the first parameter transmission or put it in the structure?

As the first parameter of the function

advantage:

  1. Clarity:WillcontextAs the first parameter, the context dependence of function execution is clearly indicated, enhancing the readability and intent expression of the code. This approach is in line with Go's design philosophy, that is, explicit is superior to implicit.

  2. Easy to test: It is easy to create and pass a custom one during testingExamples, which facilitates control of timeout and cancellation logic in tests, without modifying function signatures or structure definitions.

  3. Standard consistency: Go's standard library widely adopts this model, for examplenet/httpIn the packageServeHTTPmethod. Following this standard makes the code style unified and easy for other developers to understand and maintain.

shortcoming:

  1. Parameter list growth: For functions with more parameters, add an additional oneIt may make the function signature appear verbose, especially when multiple functions are called in nested, each layer needs to be passed.context

  2. Invasive: Although it increases flexibility, it also means that every function that needs to consider cancellation or timeout logic needs to be adjusted, which is costly to improve existing code bases.

Embed into the structure

advantage:

  1. Reduce function signature complexity:WillEmbedding into a structure as a field can reduce the number of function parameters and make the function signature more concise.

  2. Encapsulation: For services with complex internal logic,contextHidden inside the structure, it can provide more abstract and friendly interfaces to the outside and improve the encapsulation of the code.

shortcoming:

  1. Increased test complexity: If thecontextFields are not public, and when testing, it may need to pass specific context information by constructing specific structure instances, which may complicate the test code.

  2. Reduced flexibility:oncecontextAs part of the structure, the function loses direct control when calledcontext, such as the inability to easily change the timeout time or cancel the policy at runtime.

Practical advice

  • General Operation: For most cases, follow the recommendations of the Go standard library,contextPassing as the first parameter of a function is the best choice. Doing this not only reflects the simplicity and clarity of Go, but also facilitates maintenance and testing.

  • Highly encapsulated services: When designing highly encapsulated internal services or complex APIs, you can considercontextEmbed into a structure, especially when the context needs to be managed throughout the service life cycle. But the relationship between packaging and testing convenience needs to be weighed.

  • Mixed use: In some scenarios, you may find that combining the two works better. For example, during the service initialization phase,contextAs a structure field management, in the specific operation functions of the service, thecontextPass as the first parameter to maintain operation flexibility.

In short, selectcontextWhere to place it should be based on the specific requirements of the project, the readability and maintenance of the code. Either way, the key is to understand and make the most ofcontextmechanism to improve program robustness and maintainability.

The above is the detailed content of the implementation principle of Context in Go and the correct way to use it. For more information about the implementation principle of Go Context and its use, please pay attention to my other related articles!