Leapcell: The Best Serverless Platform for Golang app Hosting

1. What is Context?

Simply put, Context is an interface in the standard library introduced in Go version 1.7. Its definition is as follows:

type Context interface {
   Deadline() (deadline time.Time, ok bool)
   Done() <-chan struct{}
   Err() error
   Value(key interface{}) interface{}
}

This interface defines four methods:

  • Deadline: Sets the time when the context.Context is cancelled, i.e., the deadline.
  • Done: Returns a read - only channel. When the Context is cancelled or the deadline is reached, this channel will be closed, indicating the end of the Context's chain. Multiple calls to the Done method will return the same channel.
  • Err: Returns the reason for the end of the context.Context. It will only return a non - null value when the channel returned by Done is closed. There are two cases for the return value:
    • If the context.Context is cancelled, it returns Canceled.
    • If the context.Context times out, it returns DeadlineExceeded.
  • Value: Retrieves the value corresponding to the key from the context.Context, similar to the get method of a map. For the same context, multiple calls to Value with the same key will return the same result. If there is no corresponding key, it returns nil. Key - value pairs are written through the WithValue method.

2. Creating Context

2.1 Creating the Root Context

There are mainly two ways to create the root context:

context.Background()
context.TODO()

Analyzing the source code, there isn't much difference between context.Background and context.TODO. Both are used to create the root context, which is an empty context without any functionality. However, in general, if the current function does not have a context as an input parameter, we usually use context.Background to create a root context as the starting context to pass down.

2.2 Creating Child Contexts

After the root context is created, it has no functionality. To make the context useful in our programs, we rely on the With series of functions provided by the context package for derivation.
There are mainly the following derivation functions:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

Based on the current context, each With function creates a new context. This is similar to the tree structure we are familiar with. The current context is called the parent context, and the newly derived context is called the child context. Through the root context, four types of contexts can be derived using the four With series methods. Each context can continue to derive new child contexts by calling the With series methods in the same way, making the whole structure look like a tree.

3. What is the Use of Context?

Context mainly has two uses, which are also commonly used in projects:

  • For concurrent control, to gracefully exit goroutines.
  • For passing context information.

In general, Context is a mechanism for passing values and sending cancel signals between parent and child goroutines.

3.1 Concurrent Control

For a typical server, it runs continuously, waiting to receive requests from clients or browsers and respond. Consider this scenario: in a backend microservice architecture, when a server receives a request, if the logic is complex, it won't complete the task in a single goroutine. Instead, it will create many goroutines to work together to handle the request. After a request comes in, it first goes through an RPC1 call, then to RPC2, and then two more RPCs are created and executed. There is another RPC call (RPC5) inside RPC4. The result is returned after all RPC calls are successful. Suppose an error occurs in RPC1 during the entire call process. Without a context, we would have to wait for all RPCs to finish before returning the result, which actually wastes a lot of time. Because once an error occurs, we can directly return the result at RPC1 without waiting for the subsequent RPCs to complete. If we return a failure directly at RPC1 and don't wait for the subsequent RPCs to continue, then the execution of the subsequent RPCs is actually meaningless and just wastes computing and I/O resources. After introducing the context, we can handle this problem well. When the child goroutines are no longer needed, we can notify them to gracefully close through the context.

3.1.1 context.WithCancel

The method is defined as follows:

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

The context.WithCancel function is a cancellation control function. It only takes a context as a parameter and can derive a new child context and a cancellation function CancelFunc from the context.Context. By passing this child context to new goroutines, we can control the closure of these goroutines. Once we execute the returned cancellation function CancelFunc, the current context and its child contexts will be cancelled, and all goroutines will receive the cancellation signal synchronously.
Usage example:

package main

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

func main() {
   ctx, cancel := context.WithCancel(context.Background())
   go Watch(ctx, "goroutine1")
   go Watch(ctx, "goroutine2")

   time.Sleep(6 * time.Second)   // Let goroutine1 and goroutine2 run for 6 seconds
   fmt.Println("end working!!!")
   cancel()  // Notify goroutine1 and goroutine2 to close
   time.Sleep(1 * time.Second)
}

func Watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Printf("%s exit!\n", name) // After the main goroutine calls cancel, a signal will be sent to the ctx.Done() channel, and this part will receive the message
         return
      default:
         fmt.Printf("%s working...\n", name)
         time.Sleep(time.Second)
      }
   }
}

Running result:

goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
end working!!!
goroutine1 exit!
goroutine2 exit!

ctx, cancel := context.WithCancel(context.Background()) derives a ctx with a return function cancel and passes it to the child goroutines. In the next 6 seconds, since the cancel function is not executed, the child goroutines will always execute the default statement and print the monitoring information. After 6 seconds, cancel is called. At this time, the child goroutines will receive a message from the ctx.Done() channel and execute return to end.

3.1.2 context.WithDeadline

The method is defined as follows:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

context.WithDeadline is also a cancellation control function. The method has two parameters. The first parameter is a context, and the second parameter is the deadline. It also returns a child context and a cancellation function CancelFunc. When using it, before the deadline, we can manually call the CancelFunc to cancel the child context and control the exit of the child goroutines. If we haven't called the CancelFunc by the deadline, the Done() channel of the child context will also receive a cancellation signal to control the exit of the child goroutines.
Usage example:

package main

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

func main() {
   ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(4*time.Second)) // Set the timeout to 4 seconds from the current time
   defer cancel()
   go Watch(ctx, "goroutine1")
   go Watch(ctx, "goroutine2")

   time.Sleep(6 * time.Second)   // Let goroutine1 and goroutine2 run for 6 seconds
   fmt.Println("end working!!!")
}

func Watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Printf("%s exit!\n", name) // Receive the signal after 4 seconds
         return
      default:
         fmt.Printf("%s working...\n", name)
         time.Sleep(time.Second)
      }
   }
}

Running result:

goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine1 exit!
goroutine2 exit!
end working!!!

We didn't call the cancel function, but after 4 seconds, the ctx.Done() in the child goroutines received the signal, printed exit, and the child goroutines exited. This is how to use WithDeadline to derive a child context.

3.1.3 context.WithTimeout

The method is defined as:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

context.WithTimeout is similar to context.WithDeadline in function. Both are used to cancel the child context due to timeout. The only difference is in the second parameter passed. The second parameter passed by context.WithTimeout is not a specific time but a time duration.
Usage example:

package main

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

func main() {
   ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
   defer cancel()
   go Watch(ctx, "goroutine1")
   go Watch(ctx, "goroutine2")

   time.Sleep(6 * time.Second)   // Let goroutine1 and goroutine2 run for 6 seconds
   fmt.Println("end working!!!")
}

func Watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Printf("%s exit!\n", name) // After the main goroutine calls cancel, a signal will be sent to the ctx.Done() channel, and this part will receive the message
         return
      default:
         fmt.Printf("%s working...\n", name)
         time.Sleep(time.Second)
      }
   }
}

Running result:

goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine1 exit!
goroutine2 exit!
end working!!!

The program is very simple. It is basically the same as the previous example code for context.WithDeadline, except that the method for deriving the context is changed to context.WithTimeout. Specifically, the second parameter is no longer a specific time but a specific time duration of 4 seconds. The execution result is also the same.

3.1.4 context.WithValue

The method is defined as:

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

The context.WithValue function creates a child context from the parent context for value passing. The function parameters are the parent context, a key - value pair (key, val). It returns a context. In projects, this method is generally used for passing context information, such as the unique request ID and trace ID, for link tracing and configuration passing.
Usage example:

package main

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

func func1(ctx context.Context) {
   fmt.Printf("name is: %s", ctx.Value("name").(string))
}

func main() {
   ctx := context.WithValue(context.Background(), "name", "leapcell")
   go func1(ctx)
   time.Sleep(time.Second)
}

Running result:

name is: leapcell

Leapcell: The Best Serverless Platform for Golang app Hosting

Finally, I recommend a platform that is most suitable for deploying Golang services: Leapcell

1. Multi - Language Support

  • Develop with JavaScript, Python, Go, or Rust.

2. Deploy unlimited projects for free

  • Pay only for usage — no requests, no charges.

3. Unbeatable Cost Efficiency

  • Pay - as - you - go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

4. Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real - time metrics and logging for actionable insights.

5. Effortless Scalability and High Performance

  • Auto - scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the documentation!

Leapcell Twitter: https://x.com/LeapcellHQ

Author Of article : Leapcell Read full article