First Look at Go Context -- (From Draft)

My note from the draft, written in points, hope it will help you understand the context better.

Go Context

  • use context to gather additional information about the environment they're being executed in.

lets get into it :)

package main

import "context"
import "fmt"

func doSomething(ctx context.Context){
    fmt.Println("Doing something", ctx)
}

func main(){
    ctx := context.TODO()
    doSomething(ctx)
}
  • Here we have used, context.TODO function, on of two ways to create an empty (or starting) context.
  • Question is can i pass anything here? -- No
ctx:= context.JPT();

you can't do this :D

  • In the function accepting context, it is recommended to pass context as the first argument. As followed in go standard library.

context.Background()

  • creates empty context like context.TODO
  • designed to be used where you intend to start a known context.
  • both function do the same: they return empty context that can be used as a context.Context
  • diff? -- how you signal your intent ? -- if you are not sure what to use, context.Background() is the default option.

Using data within a context

  • ability to access data stored inside the context
  • by adding data to the context and passing the context from function to function, each layer of the program can add additional information about what's happening.

Add Value

  • To add the new value to the context use context.withValue function.
  • parameters of context.WithValue
1. parent i.e `context.Context`
2. key
3. value
  • the key and Value can be of any type
  • Return a new context.Context with the value added to it.

Accessing Value with Value

syntax: ctx.Value("myKey")


package main

import "context"
import "fmt"

func doSomething(ctx context.Context){
    fmt.Printf("Doing something : %s \n", ctx.Value("myKey"))

    // context.WithValue(ctx, "fun", "doSomething")
}

func main(){
    ctx := context.Background()
    ctx = context.WithValue(ctx, "myKey", "myValue")
    doSomething(ctx)

    // funcUsed := ctx.Value("fun");
    // fmt.Println("Function Used ", funcUsed)
}

Here i have tried whether i can get back the value set by the child function to its parent function or caller function.

If you see the commented code, we are trying that. Answer is NO, you will get Nil as the answer.

If you want to return the value then you can provide values in another context. I am not sure about its use case but you can simply use something like this.

package main

import "context"
import "fmt"

func doSomething(ctx context.Context) context.Context {
    fmt.Printf("Doing something : %s \n", ctx.Value("myKey"))
    aCtx := context.WithValue(ctx, "fun", "doSomething")
    return aCtx;
}

func main(){
    ctx := context.Background()
    ctx = context.WithValue(ctx, "myKey", "myValue")
    funCtx:= doSomething(ctx)
    fmt.Printf("Which function I have called : %s \n", funCtx.Value("fun"))
}

Reason:

  1. Values stored in the specific context are immutable, i.e they can't be changed.
  2. When calling context.Value, you pass the parent context and get new one. This function didn't modify the context you have provided.
  3. It wrapped our parent context inside another one with the new value.

To explain this, lets have another example from article

package main

import "context"
import "fmt"

func doSomething(ctx context.Context) {
    fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))

    anotherCtx := context.WithValue(ctx, "myKey", "anotherValue")
    doAnother(anotherCtx)

    fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))
}

func doAnother(ctx context.Context) {
    fmt.Printf("doAnother: myKey's value is %s\n", ctx.Value("myKey"))
}

func main(){
    ctx := context.Background()
    ctx = context.WithValue(ctx, "myKey", "myValue")
    doSomething(ctx)
}

And the possible output will be like:

doSomething: myKey's value is myValue
doAnother: myKey's value is anotherValue
doSomething: myKey's value is myValue

Overuse

  • context can be powerful tool to use but it shouldn't be used as the replacement for the arguments in the function.
  • Rule of thumb is you should always use arguments to the function for business logic.
  • Additional information can be passed with context.

Ending a Context

  • Another powerful feature of the context is that signaling function that context is ended and anything you (function) is doing related to context can stop or ended.
  • context.Context provides a method called Done that can be checked to see whether a context has ended or not.
  • This method return channel that is closed when the context is done, and any functions watching for it to be closed will know they should consider their execution context completed and should stop any processing related to that context.
  • Done method works because no values are ever written to its channel.
  • Periodically checking if done can help you ...
  • Done channel and select statement goes even further by allowing you to send data to or receive data from other channels simultaneously.

  • select statement doc: go.dev/ref/spec#Select_statements

Select

  • select statement in Go is used to allow a program to try reading from or writing to a number of channels all at the same time.
  • Only one channel operation happens per select statement, but when performed in a loop, the program can do a number of channel operations when one becomes available.
  • Each case statement can be either a channel read or write operation, and select statement will block until one of the case statements can be executed.
  • you can use default statement that will be executed immediately if none of the other case statements can be executed.
  • its works similar to switch statement but for channels.

For example


ctx:= context.Background()
resultCh := make(chan *WorkResult)

for {
    select {
        case <- ctx.Done():
            // The context is over, stop
            return
        case result := <- resultCh:
            // process the result received
    }
}

Lets see the full example here:

package main

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

func doSomething(ctx context.Context) {
    ctx, cancelCtx := context.WithCancel(ctx)

    printCh := make(chan int)
    go doAnother(ctx, printCh)

    for num := 1; num <= 3; num++ {
        printCh <- num
    }

    cancelCtx()

    time.Sleep(100 * time.Millisecond)

    fmt.Printf("doSomething: finished\n")
}

func doAnother(ctx context.Context, printCh <-chan int) {
    for {
        select {
        case <-ctx.Done():
            if err := ctx.Err(); err != nil {
                fmt.Printf("doAnother err: %s\n", err)
            }
            fmt.Printf("doAnother: finished\n")
            return
        case num := <-printCh:
            fmt.Printf("doAnother: %d\n", num)
        }
    }
}

func main(){
    ctx := context.Background()
    ctx = context.WithValue(ctx, "myKey", "myValue")
    doSomething(ctx)
}

Context Deadline


func doSomething(ctx context.Context) {
    deadline := time.Now().Add(1500 * time.Millisecond)
    ctx, cancelCtx := context.WithDeadline(ctx, deadline)
    defer cancelCtx()

    printCh := make(chan int)
    go doAnother(ctx, printCh)

    for num := 1; num <= 3; num++ {
        select {
        case printCh <- num:
            time.Sleep(1 * time.Second)
        case <-ctx.Done():
            break
        }
    }

    cancelCtx()

    time.Sleep(100 * time.Millisecond)

    fmt.Printf("doSomething: finished\n")
}

Context with TimeLimit

func doSomething(ctx context.Context) {
    ctx, cancelCtx := context.WithTimeout(ctx, 1500*time.Millisecond)
    defer cancelCtx()

}

Refs:

  1. digital ocean: digitalocean.com/community/tutorials/how-to..
  2. Reddit thread: reddit.com/r/golang/comments/y2voom/what_ar..

Few AI feedbacks:

Q1. how to avoid race condition while writing database by goroutines with a simple example please.

Answer with its explanation:

  • who cares about explanation, I only care about the code :)
  • Seems pretty fair.
package main

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

type Database struct {
    data map[string]string
    mu   sync.Mutex
}

func main() {
    db := &Database{
        data: make(map[string]string),
    }

    // Create a wait group to wait for all goroutines to finish
    var wg sync.WaitGroup

    // Number of goroutines
    numWorkers := 10

    // Add the number of goroutines to the wait group
    wg.Add(numWorkers)

    for i := 0; i < numWorkers; i++ {
        go func(index int) {
            defer wg.Done()

            // Simulate some work
            time.Sleep(time.Millisecond)

            // Acquire a lock before accessing the database
            db.mu.Lock()
            defer db.mu.Unlock()

            // Perform the database write
            key := fmt.Sprintf("key-%d", index)
            value := fmt.Sprintf("value-%d", index)
            db.data[key] = value

            fmt.Printf("Goroutine %d wrote to the database: %s -> %s\n", index, key, value)
        }(i)
    }

    // Wait for all goroutines to finish
    wg.Wait()

    // Print the final state of the database
    fmt.Println("\nFinal state of the database:")
    for key, value := range db.data {
        fmt.Printf("%s -> %s\n", key, value)
    }
}

Q2. how can i use context for the same use case?

package main

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

type Database struct {
    data map[string]string
    mu   sync.Mutex
}

func main() {
    db := &Database{
        data: make(map[string]string),
    }

    // Create a wait group to wait for all goroutines to finish
    var wg sync.WaitGroup

    // Number of goroutines
    numWorkers := 10

    // Add the number of goroutines to the wait group
    wg.Add(numWorkers)

    // Create a context with cancellation
    ctx, cancel := context.WithCancel(context.Background())

    for i := 0; i < numWorkers; i++ {
        go func(ctx context.Context, index int) {
            defer wg.Done()

            // Simulate some work
            time.Sleep(time.Millisecond)

            // Acquire a lock before accessing the database
            db.mu.Lock()
            defer db.mu.Unlock()

            // Check if the context is canceled
            select {
            case <-ctx.Done():
                fmt.Printf("Goroutine %d canceled\n", index)
                return
            default:
                // Perform the database write
                key := fmt.Sprintf("key-%d", index)
                value := fmt.Sprintf("value-%d", index)
                db.data[key] = value
                fmt.Printf("Goroutine %d wrote to the database: %s -> %s\n", index, key, value)
            }
        }(ctx, i)
    }

    // Cancel the context after a certain duration
    go func() {
        time.Sleep(5 * time.Millisecond)
        cancel()
    }()

    // Wait for all goroutines to finish or context cancellation
    wg.Wait()

    // Print the final state of the database
    fmt.Println("\nFinal state of the database:")
    for key, value := range db.data {
        fmt.Printf("%s -> %s\n", key, value)
    }
}