Golang Channel Use Cases Explained with Examples

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.

Advertisement

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:

Advertisement
$ 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.

Advertisement

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.

NOTE:

Buffered channel must be closed for it to be iterated

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:

Advertisement
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 using fmt.Println.
  • If ch2 is in ready to be read state, then its value will be read and assigned to the variable x before printing the value of x.
  • If ch3 is ready to be sent to, then the value y is sent to it before printing the value of y.
  • Finally, if no cases are ready, the default statements will be executed. If there’s no default, then the select 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:-

Advertisement
$ 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.

Advertisement

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 contextpackage, 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

 

Didn't find what you were looking for? Perform a quick search across GoLinuxCloud

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment

X