[Golang] Context

2021-12-13 hit count image

Let's see what the Context is and how to define and use the Context in Golang.

Outline

In this blog post, I will show you how to control the work flow(Goroutine) by the Context. You can see the full source code of this blog post on the link below.

If you want to know details about the Goroutine and channel, please read the blog posts below.

Context

In Golang, we can use the context as the role like the statement of work(Goroutine) to set the duration of the work or cancel the work.

In Golang, you can define the context by using the context package like the below.

import "context"

// Cancel
ctx, cancel := context.WithCancel(context.Background())
// Deadline
ctx, cancel := context.WithDeadline(context.Background(), TIME)
// Timeout
ctx, cancel := context.WithTimeout(context.Background(), DURATION)

WithCancel

When the context is canceled or timeout, the Done of the context will be called. In here, I will introduce how to exit the context by cacnel.

ctx, cancel := context.WithCancel(context.Background())

To check this, create the main.go file and modify it like the below.

package main

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

var wg sync.WaitGroup

func main() {
  wg.Add(1)
  ctx, cancel := context.WithCancel(context.Background())

  go PrintTick(ctx)

  time.Sleep(5 * time.Second)
  cancel()

  wg.Wait()
}

func PrintTick(ctx context.Context) {
  tick := time.Tick(time.Second)
  for {
    select {
    case <-ctx.Done():
      fmt.Println("Done:", ctx.Err())
      wg.Done()
      return
    case <-tick:
      fmt.Println("tick")
    }
  }
}

When you run the above code, you will see the following output.

# go run main.go
tick
tick
tick
tick
tick
Done: context canceled

As you can see the code, we added the WaitGroup to make the main Goroutine to wait for the sub Goroutine exits.

var wg sync.WaitGroup

func main() {
  wg.Add(1)
  ...
  wg.Wait()
}

And then, we created a context by using the context package, and passed it to the sub Goroutine.

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

  go PrintTick(ctx)
  ...
}

The PrintTick function that is executed on the sub Goroutine has a channel created by the Tick function of the time package that makes signals by every second. Also, it has the switch statement to wait for the data from the Done channel and the Tick function channel.

func PrintTick(ctx context.Context) {
  tick := time.Tick(time.Second)
  for {
    select {
    case <-ctx.Done():
      fmt.Println("Done:", ctx.Err())
      wg.Done()
      return
    case <-tick:
      fmt.Println("tick")
    }
  }
}

When a signal comes from the Tick function channel, the tick string is printed on the screen, and when a signal comes fron the Done function channel, the Done text and the reason(ctx.Err()) are printed. After then, the Done function of WaitGroup is called to notify the sub Goroutine exits to the main Goroutine.

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

  go PrintTick(ctx)

  time.Sleep(5 * time.Second)
  cancel()
  ...
}

The main Goroutine waits for 5 seconds after running the PrintTick function and calls the cancel to cancel the context after 5 seconds. So, the tick is printed 5 times on the screen, and the context exit reason is printed. And then, we can see the program exit.

WithDeadline

The Deadline of the context is decided when the workflow(Goroutine) should exit.

ctx, cancel := context.WithDeadline(context.Background(), TIME)

To check this, modify the main.go file like the below.

package main

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

var wg sync.WaitGroup

func main() {
  wg.Add(1)

  d := time.Now().Add(3 * time.Second)
  ctx, cancel := context.WithDeadline(context.Background(), d)

  go PrintTick(ctx)

  time.Sleep(time.Second * 5)
  cancel()

  wg.Wait()
}

func PrintTick(ctx context.Context) {
  tick := time.Tick(time.Second)
  for {
    select {
    case <-ctx.Done():
      fmt.Println("Done:", ctx.Err())
      wg.Done()
      return
    case <-tick:
      fmt.Println("tick")
    }
  }
}

When the code is run, you will see the following result.

# go run main.go
tick
tick
tick
Done: context deadline exceeded

The code is from the example of the cancel above. Just, when creating a context, we set the deadline to specify the terminate the context 3 seconds later from now.

d := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)

So, the tick is printed 3 times on the screen, and we can see the context exit by the deadline. However, the main Goroutine is kept for 5 seconds and we can see the program exit.

func main() {
  ...
  d := time.Now().Add(3 * time.Second)
  ctx, cancel := context.WithDeadline(context.Background(), d)
  ...
  time.Sleep(time.Second * 5)
  cancel()
  ...
}

Eve if you set the deadline to exit the context, you must use the cacnel function to close the context.

WithTimeout

The Timeout of the context is decided how long the workflow(Goroutine) should be kept.

ctx, cancel := context.WithTimeout(context.Background(), TIME)

Toe check this, modify main.go file like the below.

package main

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

var wg sync.WaitGroup

func main() {
  wg.Add(1)

  ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

  go PrintTick(ctx)

  time.Sleep(time.Second * 5)
  cancel()

  wg.Wait()
}

func PrintTick(ctx context.Context) {
  tick := time.Tick(time.Second)
  for {
    select {
    case <-ctx.Done():
      fmt.Println("Done:", ctx.Err())
      wg.Done()
      return
    case <-tick:
      fmt.Println("tick")
    }
  }
}

When you execute the code, you can see the following result.

# go run main.go
tick
tick
tick
Done: context deadline exceeded

As you can see the result, the result is the same as when we use the Deadline. The difference between the deadline and timeout is that the deadline is when the workflow should exit and the timeout is how long the workflow should be kept.

// Deadline
d := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

When you use the timeout, you must use the cancel function to close the context, too.

WithValue

The WithValue of the context is used for passing the data to the sub Goroutine.

ctx := context.WithValue(context.Background(), KEY, VALUE)

v := ctx.Value(KEY)

To check this, modify the main.go file like the below.

package main

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

var wg sync.WaitGroup

func main() {
  wg.Add(1)

  ctx := context.WithValue(context.Background(), "v", 3)

  go square(ctx)

  wg.Wait()
}

func square(ctx context.Context) {
  if v := ctx.Value("v"); v != nil {
    n := v.(int)
    fmt.Println("Square:", n*n)
  }
  wg.Done()
}

When you execute the code, you can see the following result.

# go run main.go
Square: 9

Context wrapping

In Golang, you can warp the context like the below.

ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, "key", "value")
ctx = context.WithValue(ctx, "key2", "value2")

To check this, modify the main.go file like the below.

package main

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

var wg sync.WaitGroup

func main() {
  wg.Add(1)
  ctx, cancel := context.WithCancel(context.Background())
  ctx = context.WithValue(ctx, "s", 2)

  go PrintTick(ctx)

  time.Sleep(5 * time.Second)
  cancel()

  wg.Wait()
}

func PrintTick(ctx context.Context) {
  tick := time.Tick(time.Second)

  if v := ctx.Value("s"); v != nil {
    s := v.(int)
    tick = time.Tick(time.Duration(s) * time.Second)
  }

  for {
    select {
    case <-ctx.Done():
      fmt.Println("Done:", ctx.Err())
      wg.Done()
      return
    case <-tick:
      fmt.Println("tick")
    }
  }
}

When the code is run, you will see the following result.

# go run main.go
tick
tick
Done: context canceled

We used the WithValue to wrap the context from the WithCancel.

func main() {
  ...
  ctx, cancel := context.WithCancel(context.Background())
  ctx = context.WithValue(ctx, "s", 2)
  ...
}

We passed the value via the wrapping context, and if there is a value in the context, the tick string is printed every 1 second instead of 2 seconds.

func PrintTick(ctx context.Context) {
  tick := time.Tick(time.Second)

  if v := ctx.Value("s"); v != nil {
    s := v.(int)
    tick = time.Tick(time.Duration(s) * time.Second)
  }
  ...
}

Like this, we can wrap the context to make the statement of work.

Completed

Done! we’ve seen What the Context is in Golang. And, we’ve seen how to define and use the Context by WithCacnel, WithDeadline, and WithTimeout. Also, we’ve learned how to pass the value via the Context by WithValue and how to wrap the Context. The Context is frequently used to manage the Goroutine, so let’s remember it well!

Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!

App promotion

You can use the applications that are created by this blog writer Deku. 지금 보고 계신 블로그를 작성하는 Deku가 개발한 앱을 한번 사용해보세요.
Deku created the applications with Flutter.

If you have interested, please try to download them for free.

Posts