Table of Contents
Overview on golang channel
In Go, Channel is a communication pipeline through which goroutines share/communicate data with each other. Each channel is of a particular type element to enhance easy programming style and debugging.
Steps to create a channel in Go
Channels may be created using the make function. Each channel can transmit values of a specific type, called its element type. Channel types are written using the chan keyword followed by their element type.
We use to make similar to slice and map to create channels. Channels are of specific data type represented by [value-type]
of the value to be shared i.e sent or received a cross goroutine. e.g ch := make(chan int).
ch := make(chan [value-type])
Here,
ch <- values // sending on a channel
value := <- ch // Receiving on a channel and assigning it to value
<-ch // Receiving on a channel and discarding the result
close(ch) // Closing a channel. It means that channel can't send any data
Example
// main.go
package main
import (
"fmt"
"os"
"strconv"
)
// calculating total of values
func TotalValue(array []int, ch chan int) {
total := 0
for _, val := range array {
total += val
}
// this is a sending channel
ch <- total // send total to channel ch
}
func main() {
ch := make(chan int)
var err error
val := os.Args[1:]
nums := make([]int, len(val))
for i := 0; i < len(val); i++ {
if nums[i], err = strconv.Atoi(val[i]); err != nil {
fmt.Println(err)
}
go TotalValue(nums, ch)
values := <-ch // receive from ch
fmt.Println("Total: ", values)
}
}
Explanation:-
We have created a func TotalValue(array []int, ch chan int) {}
that calculates totals of a given array of inputs. using the channel to send and receive values.
Output:
$ go run main.go 1 3 4 5 6 6 7 7 12 34 24
Total: 1
Total: 4
Total: 8
Total: 13
Total: 19
Total: 25
Total: 32
Total: 39
Total: 51
Total: 85
Total: 109
Directional channel in Go
In Go, channels are bidirectional by default, but these channels can be directional in such a way that they can only send or receive data from goroutines. This is always denoted by the arrow <-
the symbol we discussed above
For example on how to create send or receive only channel
Sending channel only
func TotalValue(array []int, ch chan <- int) {
total := 0
for _, val:= range array {
total += val
}
// this is a sending channel
ch <- total // send total to channel ch
}
Explanation:-
ch chan <- int
Using the arrow symbol indicates that our channel can only send data of type integer
Receiving channel only
ch <- chan int
indicates that our channel can only receive data of type integer.
Unspecified channel
ch:= make(chan int)
this channel is declared without defining direction i.e sending or receiving, thus it can either send or receive data but they are of specific data type. Mostly, they are defined in a function and can be used in any direction user needs.
Go channel use cases
In Go, With Go Channels we can achieve asynchronous and concurrency programming. This synchronization technique has a wider range of uses such as async/await patterns and actor models. Below are uses of the channel in Golang
Sending/scheduling Notification
In Go, Notification is considered as one of a kind request from a user or sending a response from the server that returns values that are not important. We use a blank struct type struct {}
as the element type of notification channel since its size is zero and hence doesn't consume memory.
Example of schedule notification
package main
import (
"fmt"
"time"
)
func notification(t time.Duration) <-chan struct{} {
ch := make(chan struct{}, 1)
go func() {
time.Sleep(t)
ch <- struct{}{}
}()
return ch
}
func main() {
fmt.Println("Go")
<-notification(time.Second)
fmt.Println("Linux")
<-notification(time.Second)
fmt.Println("Cloud")
}
Output:-
$ go run notification.go
Go
Linux
Cloud
Explanation
func notification(t time.Duration) <-chan struct{} {}
mimics the After
function in the time
standard package. it accepts time...
second, millisecond, minutes, hours, etc and it's combined with time.Sleep(duration)
to ensure that the program is not blocked thus it executes concurrently.
You can learn more about Golang channels: Go channel Uses cases
Use of range in Go channels
In Go, Range is mainly used with a for loop flow control statement to iterate over a channel to retrieve/read values in it. It maintains the FIFO principle used in queues, FIRST IN, FIRST OUT. With range, we can use different types of channels, from unbuffered to buffered.
Example use of range in a channel
package main
import "fmt"
func main() {
// create buffered channel
ch := make([]chan string,3 )
close(ch)
for data := range ch {
fmt.Println(data)
}
}
Select statement in Go channels
In Go, the Select statement is always useful whenever numerous numbers of goroutines are sharing data. Each channel has to complete execution first before the next.
Syntax of select statement
Go’s select statements are a little like switch statements that provide a convenient mechanism for multiplexing communications with multiple channels. The syntax for select is very similar to switch, with some number of case statements that specify code to be executed upon a successful send or receive operation:
select {
case <-ch1: // Discard received value
fmt.Println("Got something")
case x := <-ch2: // Assign received value to x
fmt.Println(x)
case ch3 <- y: // Send y to channel
fmt.Println(y)
default:
fmt.Println("None of the above")
}
Explanation:
Here there are three possible cases specified with three different conditions.
- If the channel
ch1
is in ready to be read state, then its value will be read (and then discarded) and the text “Got something
” will be printed usingfmt.Println
. - If
ch2
is in ready to be read state, then its value will be read and assigned to the variablex
before printing the value ofx
. - If
ch3
is ready to be sent to, then the valuey
is sent to it before printing the value ofy
. - Finally, if no cases are ready, the
default
statements will be executed. If there’s no default, then theselect
will block until one of its cases is ready, at which point it performs the associated communication and executes the associated statements. If multiple cases are ready, select will execute one at random.
comma ok idiom
In Golang, the comma ok idiom
is mostly used as a for loop statement which iterates over a map[type]type{}
list to retrieve values when key is passed.
The ok
is true if there was a key on the map. So there are two forms of map access built into the language and two forms of this statement. I have provided a simpler code below demonstrating the map[string]string{}
and use of comma ok idiom.
For Example:-
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
c <- 42
close(c)
}()
// ok should be true as we have some content in channel
v, ok := <-c
fmt.Println(v, ok)
// ok will be false as channel is empty here
v, ok = <-c
fmt.Println(v, ok)
}
Output:-
$ go run main.go
42 true
0 false
Access Golang playground using URL playground
Fan in and fan out in golang channel
In Golang, Fan In and Fan Out, works together to process data from either a single/multiple stream or pipelines into one channel mainly converge and diverging of the task. A good scenario here could be a computer processor which contains several number of tasks to be executed at simultaneously. once job done notification is shared via bus and registries are updated. Fan-In and Fan-Out works the same way. Below is an example:-
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
tasksNo, _ := strconv.Atoi(os.Args[1])
fmt.Printf("You entered %d number of tasks to be executed : ", tasksNo)
//int chanel
chanInt := make(chan int)
//complete task channel return bool
done := make(chan bool)
// beffered channel of fanout no blocking
fan_Out_Chan := make([]chan map[int]int, tasksNo)
// Unbuffered channel which holds a limited number of tasks
fan_In_Chan := make(chan map[int]int)
// Random 2d array value generated
go func(num chan<- int) {
for i := 0; i < 10; i++ {
for j := 0; j < 5; j++ {
num <- j
}
}
close(num)
}(chanInt)
for s := 0; s < tasksNo; s++ {
fan_Out_Chan[s] = fan_out(chanInt, done, s)
}
// fan_In combines all results
fan_In(done, fan_In_Chan, fan_Out_Chan...)
go func() {
for s := 0; s < tasksNo; s++ {
<-done
}
close(fan_In_Chan)
}()
for key := range fan_In_Chan {
fmt.Println(key)
}
}
func fan_out(chan_Int 1; j-- {
out *= j
}
chanOut <- map[int]int{i: out}
intervals++
}
fmt.Println("Goroutine Fan_Out ", task_Name, "processed", intervals, "items")
close(chanOut)
done <- true
fmt.Println("Goroutine Fan_Out", task_Name, "is finished")
}()
return chanOut
}
func fan_In(done chan bool, fan_In_Chan chan map[int]int, result_chan ...chan map[int]int) {
for key, value := range result_chan {
go func(value chan map[int]int, key int) {
intervals := 0
for i := range value {
fan_In_Chan <- i
intervals++
}
fmt.Println("Goroutine Fan_In ", key, "consumed", intervals, "items")
done <- true
}(value, key)
}
}
Output:-
$ go run main.go 3
map[1:1]
map[3:6]
Goroutine Fan_Out 0 processed 15 items
Goroutine Fan_Out 0 is finished
Goroutine Fan_In 0 consumed 15 items
Goroutine Fan_Out 1 processed 18 items
Goroutine Fan_Out 1 is finished
map[4:24]
Goroutine Fan_Out 2 processed 17 items
Goroutine Fan_In 2 consumed 17 items
Explanation:-
In the above example, We have used OS,fmt,strconv
packages. os.Args[1]
helps us to provide the number of tasks to be executed along side the program execution go run main.go 4
. Channels to ensure that the task is executed a synchronizes. fmt package provides use the fmt.Println()
function to display continuous execution of program. Go goroutines go func()
have been utilized mostly in the program, to help in executing various functions and provide results to the next channel. Fan In and Fan out work like a conveying belt, where one task is being executed from one end and the output is played on the other end. func fan_In(done chan bool, fan_In_Chan chan map[int]int, result_chan ...chan map[int]int) {}
. Its a Fan-In function, It works closely to display passed data from FanOut function. Since we are using channels to relay data across each function, it accepts a channel of type boolean, map of integers, and ellipses (...) of map of integer data type, as the arguments. it loops through the variadic results_chan channel. Its checks if all the tasks are completed through the done channel which blocks until the last one is executed. Once each task is done it's displayed. On the other hand of Fan Out we used func fan_out(chan_Int <-chan int, done chan bool, task_Name int) chan map[int]int {}
it acts as a receiving channel and listens to whether tasks are done as well as a total number of tasked passed. It acts as the engine in this task to process all tasks to be executed.
context in golang channel
In Go, context
is found everywhere in a Go program. context
the package provides us with context.Context
function which allows us to pass "context" across the whole program. We can pass the context provided by any function that calls it and its subroutes call that requires the context as an argument, i.e func ReadingContext (ctx context.Context) error {}
. To use context.Context
effectively you need to understand how to use it and when it's needed.
You can read more about context in go article context
Different aspects of using context in the Go channel
- Retrieving and storing data in the database
- Passing timeout or deadline of a certain function execution
- Sending cancellation signal
Retrieving and storing data in the database
In Go, context.Context
the function provides us with context.WithValue()
the method is used to store and retrieve values associated with the specified struct or required arguments in the context.
For example
package main
import (
"context"
"fmt"
"github.com/gorilla/mux"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
logs "log"
"net/http"
"time"
)
var logKey = &struct{}{}
func requestData(w http.ResponseWriter, r *http.Request) {
data := log.With().Str("Get_Method", r.Method).Str("original_path", r.URL.Path).Logger()
ctx := context.WithValue(r.Context(), logKey, data)
storeData(ctx)
}
func storeData(ctx context.Context) {
logger := ctx.Value(logKey).(zerolog.Logger)
logger.Debug().Msg("Data stored successfully")
}
func main() {
fmt.Print("Server starting \n")
router := mux.NewRouter()
router.HandleFunc("/handle", requestData)
srv := &http.Server{
Handler: router,
Addr: "127.0.0.1:8080",
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
logs.Fatal(srv.ListenAndServe())
}
Passing timeout or deadline of a certain function execution
In Golang, context standard library provides us with context.WithTimeout()
function which in turn allows the use of contexts for cancellation, timeouts, and passing request-scoped program.
Its implemented as follows
ctx, cancel := context.WithTimeout(context context.Background(), 2*time.Second)
defer cancel()
// ctx is passed to the program running in background.
NB// time can vary based on the need of the user.
Below is an example of context.WithTimeout()
function
package main
import (
"context"
"fmt"
"log"
"os"
"regexp"
"time"
)
func ReadingFileContent(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("timed out")
return
default:
random("test.txt")
}
time.Sleep(time.Minute)
}
}
func random(file string) {
fileData, err := os.ReadFile(file)
if err != nil {
log.Fatalln(err)
}
line := regexp.MustCompile(`\r?\n`).Split(string(fileData), -1)
for _, r := range line {
fmt.Println(r)
}
}
func main() {
fmt.Println("Go Context with channels")
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go ReadingFileContent(ctx)
select {
case <-ctx.Done():
fmt.Println("oh no, I've exceeded the deadline")
}
time.Sleep(2 * time.Second)
}
Output:
$ go run main.go
Go Context with channels
hello Golang
hello AWS
hello GoLinuxCloud
..
oh no, I've exceeded the deadline
Explanation:-
We are passing cancellation timeout for the exact amout of time the reading of a file contect should take. if it exceeds that limit a cancellation message is shared via a context in the program. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
we are passing context withtimeout
attached in it. In our select {case}
statement once a cancel notification after timeout is received from ctx.Done()
. Done returns a channel that's closed when work done on behalf of this context should be canceled. WithTimeout
arranges for Done to be closed when the timeout elapses the program is closed. func ReadingFileContent(ctx context.Context) {}
it accepts context as an arguments, which is passed to the channel as channel receiving end listen to done or timeout activity. func random(file string) {
It is a function that reads line by line the file context and it requires to be provided with the file path.
}
Sending cancellation signal
context.Context
structs contain functions that we need to control the flow of application programs. It helps us with a mechanism to listen to send a signal to tell processes that received it to stop. For example in Go, we can send a shutdown signal to stop the server from running mostly using the Golang mux framework. context package provides context.WithCancel()
API, which returns a new context configured with cancel and a function to cancel it. If you call the cancel function, the signal is sent to processes that receive the context. With a combination of OS
and context
package, os provides us with a mechanism to listen to signals sent from a particular endpoint.
For example
package main
import (
"context"
"github.com/rs/zerolog/log"
"math/rand"
"os"
"os/signal"
"syscall"
"time"
)
type Server struct{}
func (s *Server) startServer(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Print("cancel received, attempting graceful stop...")
return
default:
//awaiting time for the server response
time.Sleep(time.Duration(rand.Intn(10)) * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
//goroutine
go func() {
sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
<-sigchan
cancel()
}()
server := &Server{}
server.startServer(ctx)
log.Print("Server stopped graceful")
}
Output:-
$ go run main.go
{"level":"debug","time":"2022-08-21T19:19:49+03:00","message":"cancel received, attempting graceful stop..."}
{"level":"debug","time":"2022-08-21T19:19:49+03:00","message":"Server stopped graceful"}
Explanation:-
The server keeps on running until the stop command ctrl + c
is executed. it will stop and display that message shown above.
Summary
In Go, channels are essential for communication between goroutines. Channels support bidirectional communication i.e sending and receiving values on the same channel. This mechanism enables goroutines to synchronize without experiencing any deadlock.
Reference
channel use cases
Go channel
Context in go channel