Context About Context Package in Golang

Asheet Bhaskar
8 min readMay 29, 2021

This post is about the context package of golang and how to get started with it. We’ll also disucss the various use cases of it. After reading this blog user should have comprehensive understanding of context package of golang.

Pre-requisites

  • goroutines: goroutines are the light weight threads which can be used to execute functions concurrently with other functions. You can read more about goroutines at link
  • channels: channels can be thought of as a pipe that connect multiple concurrent goroutines. Read more about channels at link
  • defer: A defer statement postpones the execution of a function until the surrounding function returns. Read more at link

Why should we use context?

Let’s take the example of following timeline for any action.

  • An action starts at time instant t0.
  • Action completes at time instant t5.
  • This action initiates three different goroutines namely, goroutine 1, goroutine 2 and goroutine 3 etc.
  • goroutine 1 starts and completes at t1 and completes at t’1.
  • goroutine 2 starts and completes at t2 and completes at t’2.
  • goroutine 3 starts and completes at t3 and completes at t’3.

If due to some unavoidable reason if above action has to be terminated at time instant t4, It’s importnat to terminate goroutines 2 and 3 as well in order to avoid the sideeffects that’ll be caused by goroutines 2 and 3. Context provides the ability to time-out and terminate goroutines to handle such cases.

Using context | Examples

Example 1: run

func operationOne(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Println("context canceled for op-1")
return // returning not to leak the goroutine
default:
fmt.Printf("OperationOne: %d\n", n)
time.Sleep(500 * time.Millisecond)
n++
}
}
}

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

Observations from example 1

  • function operationOne is not being called in a separate goroutine, it’s blocking call and main funtion won’t exits util operationOne returns.
  • function operationOne is being passed context as argument.
  • context is not being cancelled or being mark as Done.
  • operationOne will never return as context is not being cancelled at all.

Example 2: run

func operationOne(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Println("context canceled for op-1")
return // returning not to leak the goroutine
default:
fmt.Printf("OperationOne: %d\n", n)
time.Sleep(500 * time.Millisecond)
n++
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go operationOne(ctx)
time.Sleep(5 * time.Second)
}

Observations from example 2

  • function operationOne is being called in a separate goroutine.
  • function operationOne is being passed context as argument.
  • main function will return after 5 seconds from the start of execution.
  • context will cancelled at return of main function.

Example 3: run

func operationOne(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Println("context canceled for op-1")
return // returning not to leak the goroutine
default:
fmt.Printf("OperationOne: %d\n", n)
time.Sleep(500 * time.Millisecond)
n++
}
}
}

func operationTwo(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Println("context canceled for op-2")
return // returning not to leak the goroutine
default:
fmt.Printf("OperationTwo: %d\n", n)
time.Sleep(250 * time.Millisecond)
n++
}
}
}

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
d := time.Now().Add(5000 * time.Millisecond)
ctx, cancel = context.WithDeadline(context.Background(), d)
defer cancel()
go operationOne(ctx)

d = time.Now().Add(10000 * time.Millisecond)
ctx, cancel = context.WithDeadline(context.Background(), d)
defer cancel()
go operationTwo(ctx)

time.Sleep(20 * time.Second)
}

Observations from example 3

  • function operationOne and operationTwo are being called in a separate goroutines.
  • drived context with differenty deadlines are being passed to operationOne and operationTwo functions.
  • main function will return after 20 seconds from the start of execution.
  • function operationOne will return after 5 seconds as deadline of context passed to it will be reached.
  • function operationTwo will return after 10 seconds as deadline of context passed to it will be reached.
  • main function will return only after 20 seconds even though operationOne and operationTwo will be completed in first 10 seconds.

Example 4: run

func operationOne(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Println("context canceled for op-1")
return // returning not to leak the goroutine
default:
fmt.Printf("OperationOne: %d\n", n)
time.Sleep(500 * time.Millisecond)
n++
}
}
}
func operationTwo(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Println("context canceled for op-2")
return // returning not to leak the goroutine
default:
fmt.Printf("OperationTwo: %d\n", n)
time.Sleep(250 * time.Millisecond)
n++
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
d := time.Now().Add(5000 * time.Millisecond)
ctx, _ = context.WithDeadline(context.Background(), d)
go operationOne(ctx)
d = time.Now().Add(10000 * time.Millisecond)
ctx, _ = context.WithDeadline(context.Background(), d)
go operationTwo(ctx)
time.Sleep(3 * time.Second)
}

Observations from example 4

  • function operationOne and operationTwo are being called in a separate goroutines.
  • drived context with differenty deadlines are being passed to operationOne and operationTwo functions.
  • function operationOne will return after 5 seconds as deadline of context passed to it will be reached.
  • function operationTwo will return after 10 seconds as deadline of context passed to it will be reached.
  • main function will return after first 3 seconds.

what will happen to other two goroutines?

  • This is a case of context leak. even after return of main function other two drived contexts passed to operationOne* and operationTwo will not be discarded. To avoid context leak it’s a good practice to use defer cancel() even for drived contexts

Example 5: run

type Key string  func operationOne(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Printf("context canceled for %s\n", ctx.Value(Key("op_id")))
return // returning not to leak the goroutine
default:
fmt.Printf("OperationOne: %d : opeartion_id = %s\n", n, ctx.Value(Key("op_id")))
time.Sleep(500 * time.Millisecond)
n++
}
}
}
func operationTwo(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Printf("context canceled for %s\n", ctx.Value(Key("op_id")))
return // returning not to leak the goroutine
default:
fmt.Printf("OperationTwo: %d : opeartion_id = %s\n", n, ctx.Value(Key("op_id")))
time.Sleep(250 * time.Millisecond)
n++
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
d := time.Now().Add(5000 * time.Millisecond)
ctx, cancel = context.WithDeadline(context.Background(), d)
defer cancel()
ctx = context.WithValue(ctx, Key("op_id"), "ONE")
go operationOne(ctx)
d = time.Now().Add(10000 * time.Millisecond)
ctx, cancel = context.WithDeadline(context.Background(), d)
defer cancel()
ctx = context.WithValue(ctx, Key("op_id"), "TWO")
go operationTwo(ctx)
time.Sleep(20 * time.Second)
}

Observations from example 5

  • function operationOne and operationTwo are being called in a separate goroutines.
  • drived context with differenty deadlines are being passed to operationOne and operationTwo functions.
  • a variable op_id and it’s value is also being passed with drived context in both the cases.
  • main function will return after 20 seconds from the start of execution.
  • function operationOne will return after 5 seconds as deadline of context passed to it will be reached.
  • function operationTwo will return after 10 seconds as deadline of context passed to it will be reached.
  • main function will return only after 20 seconds even though operationOne and operationTwo will be completed in first 10 seconds.

Example 6: run

type Key string  func operationOneChild(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Printf("context canceled for %s\n", ctx.Value(Key("op_id")))
return // returning not to leak the goroutine
default:
fmt.Println("Child of operation one")
time.Sleep(100 * time.Millisecond)
}
}
} func operationOne(ctx context.Context) {
n := 1
// go operationOneChild(context.WithValue(ctx, Key("op_id"), "CHILD OF ONE"))
go operationOneChild(nil)
for {
select {
case <-ctx.Done():
fmt.Printf("context canceled for %s\n", ctx.Value(Key("op_id")))
return // returning not to leak the goroutine
default:
fmt.Printf("OperationOne: %d : opeartion_id = %s\n", n, ctx.Value(Key("op_id")))
time.Sleep(500 * time.Millisecond)
n++
}
}
}
func operationTwo(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Printf("context canceled for %s\n", ctx.Value(Key("op_id")))
return // returning not to leak the goroutine
default:
fmt.Printf("OperationTwo: %d : opeartion_id = %s\n", n, ctx.Value(Key("op_id")))
time.Sleep(250 * time.Millisecond)
n++
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
d := time.Now().Add(5000 * time.Millisecond)
ctx, cancel = context.WithDeadline(context.Background(), d)
defer cancel()
ctx = context.WithValue(ctx, Key("op_id"), "ONE")
go operationOne(ctx)
d = time.Now().Add(10000 * time.Millisecond)
ctx, cancel = context.WithDeadline(context.Background(), d)
defer cancel()
ctx = context.WithValue(ctx, Key("op_id"), "TWO")
go operationTwo(ctx)
time.Sleep(20 * time.Second)
}

Observations from example 6

  • function operationOne and operationTwo are being called in a separate goroutines.
  • drived context with differenty deadlines are being passed to operationOne and operationTwo functions.
  • function operationOne will return after 5 seconds as deadline of context passed to it will be reached.
  • function operationTwo will return after 10 seconds as deadline of context passed to it will be reached.
  • main function will return only after 20 seconds even though operationOne and operationTwo will be completed in first 10 seconds.
  • nil drived context is being passed to function operationOneChild which will panic with following result.
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xf3966]
goroutine 9 [running]:
main.operationOneChild(0x0, 0x0)
/tmp/sandbox968352470/prog.go:14 +0xa6
created by main.operationOne
/tmp/sandbox968352470/prog.go:28 +0x40

Example 7: run

type Key string  func operationOneChild(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Printf("context canceled for %s\n", ctx.Value(Key("op_id")))
return // returning not to leak the goroutine
default:
fmt.Println("Child of operation one")
time.Sleep(100 * time.Millisecond)
}
}
} func operationOne(ctx context.Context) {
n := 1
go operationOneChild(context.WithValue(ctx, Key("op_id"), "CHILD OF ONE"))
// go operationOneChild(nil)
for {
select {
case <-ctx.Done():
fmt.Printf("context canceled for %s\n", ctx.Value(Key("op_id")))
return // returning not to leak the goroutine
default:
fmt.Printf("OperationOne: %d : opeartion_id = %s\n", n, ctx.Value(Key("op_id")))
time.Sleep(500 * time.Millisecond)
n++
}
}
}
func operationTwo(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done():
fmt.Printf("context canceled for %s\n", ctx.Value(Key("op_id")))
return // returning not to leak the goroutine
default:
fmt.Printf("OperationTwo: %d : opeartion_id = %s\n", n, ctx.Value(Key("op_id")))
time.Sleep(250 * time.Millisecond)
n++
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
d := time.Now().Add(5000 * time.Millisecond)
ctx, cancel = context.WithDeadline(context.Background(), d)
defer cancel()
ctx = context.WithValue(ctx, Key("op_id"), "ONE")
go operationOne(ctx)
d = time.Now().Add(10000 * time.Millisecond)
ctx, cancel = context.WithDeadline(context.Background(), d)
defer cancel()
ctx = context.WithValue(ctx, Key("op_id"), "TWO")
go operationTwo(ctx)
time.Sleep(20 * time.Second)
}

Observations from example 7

  • function operationOne and operationTwo are being called in a separate goroutines.
  • drived context with differenty deadlines are being passed to operationOne and operationTwo functions.
  • function operationOne will return after 5 seconds as deadline of context passed to it will be reached.
  • function operationTwo will return after 10 seconds as deadline of context passed to it will be reached.
  • main function will return only after 20 seconds even though operationOne and operationTwo will be completed in first 10 seconds.
  • context which is being passed to operationOneChild is non nil, non drived context. This child go routine will also be terminated on cancellation of parent context.

Things to keep in mind while using the context

  • Incoming requests to a server should create a Context.
  • Outgoing calls to servers should accept a Context.
  • The chain of function calls between them must propagate the Context.
  • Replace the current Context with the derived Context.
  • Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it.
  • Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
  • The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

Thanks for reading the blog. If you have any suggestions about plugins to use, please let me know in comments.

--

--