Photo by Pontus Wellgraf on Unsplash
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:
- Values stored in the specific context are immutable, i.e they can't be changed.
- When calling
context.Value
, you pass the parent context and get new one. This function didn't modify the context you have provided. - 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 calledDone
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 andselect
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:
- digital ocean: digitalocean.com/community/tutorials/how-to..
- 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)
}
}