Go: Contexts, Channels & Goroutines
Table of Contents
Go: Contexts, Channels & Goroutines
Concurrency
In Computer Science Concurrency
is very important because efficient resource management of core resources like Processor, Memory and Network usage. Any large complex probelem can be broken down into smaller problem tasks that can be handled concurrently. This also allows applications to be faster and scale efficiently.
When tasks can be broken down into smaller tasks, responsiveness of systems can be enhanced and also create better human interactiveness.
Concurrency is one of a fundamental aspect of go
programming language. Three core features in go
language that are very important are:
Subroutines
Also known as goroutine
, it is a lightweight execution thread and a function that executes concurrently with the rest of the program. Compared to threads, goroutines
are extremely cheap, have a very low overhead and are widely used.
Channels
It is a mechanishm that allows multiple goroutines
to communicate bi-directionally in a very effective manner completely lock-free
It can almost be viewed as a commonly used unix pipe
Context
Context provides a mechanism to control the lifecycle, cancellation, and propagation of requests across multiple goroutines.
context
is a standard package of Golang that makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request.
Examples
Code examples always makes the concepts look very clear.
Context : TODO
The simplest way of using a context
is the use of context.TODO()
.
import "context"
func doWork(ctx context.Context) {
fmt"Work Done!"
}
func main() {
ctx := context.TODO()
doWork(ctx)
}
This form of context
use is very handy if you are passing any information across layers but needs to call a API with context for immediate things t get done.
Context : WithValue
Here is an example use of adding data into context and using it in multiple layers
import (
"context"
)
func PlaceSecrets(ctx context.Context) context.Context {
return ctx.WithValue(ctx, "openapi-key", "adasd123113dsd33")
}
func doWork(ctx context.Context) {
apiKey := ctx.Value("openapi-key")
ChatGPT_APICall(apiKey)
}
func main() {
ctx := context.Background()
ctx = PlaceSecrets(ctx)
doWork(ctx)
}
Context: WithTimeout
There are many use case scenarios where systems should be designed to fail if a certain deadline is not met.
Let’s conside the following example
// forever loop that keep doing the work
func doWork(ctx context.Context) {
for {
select {
case <-ctx.Done():
// this goroutine gets this if the timeout exceeded
err := ctx.Err()
fmt.Println("Timed out: ", err)
return
default:
// kee doing this work in a forever loop
fmt.Println("Working...")
}
time.Sleep(100 * time.Millisecond)
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Let's spin off a subrutine to do the work
go doWork(ctx)
select {
case <-ctx.Done():
fmt.Println("Timeout Exceeded")
}
}
As you can see, there is good case of the use of a timeout
when a goroutine
is assigned a task to work on. If the timeout exceeds, the goroutine
terminates cleanly.
Goroutines: Special word go
Here is a very simple example
func doWork() {
for i:=0; i < 20; i++ {
fmt.Println("Working ...")
}
fmt.Println("Work over")
}
func main() {
fmt.Println("main starts")
go doWork()
fmt.Println("main sleeps")
time.Sleep(5 * time.Second)
fmt.Println("main ends")
}
As you can see, the doWork
here is spun off as if it was a thread->Create
. This is much more lightweight but it is the same concept.
Channel
package main
import "fmt"
func HttpServer(port string) {
// will contain and HTTP server listener
// Please refer to Gin, Mux frameworks
}
func ReceiveCloudMessages(dataStream chan string) {
// Cloud API to receive messages e.g. AWS SQS
str := aws.ReceiveMessage()
// Now we place it in the channel
dataStream <- str
}
func main() {
// Spin off a Webserver in a goroutine
go HttpServer(":8080")
// create a stream that received messages from a Cloud service
// generic string message types
// We also create a goroutine that receives messages from a cloud service
newStream := func() <-chan string {
edStream := make(chan string)
go ReceiveCloudMessages(edStream)
return edStream
}
dataStream := newStream()
// now we loop forever reading from the channel
// and simply printing it
for {
rcvMessage, ok := <-dataStream
if ok {
fmt.Println("Received: ", rcvMessage)
}
time.Sleep(3 * time.Second)
}
}
Here the goroutine
that receives messages from a Cloud Service ( e.g. AWS SQS ) and
places them in the channel. The main thread is looping reading throgh the channel.
It then prints the message received in the channel