programming

Channel Introduction to Golang

A channel is a link from one goroutine to another. This channel is synchronous because of blocking. Channels can be defined as variables with the keyword chan. This variable has the task of sending and receiving data.

Here is an example of a channel implementation that has 3 new goroutines executed and each gouroutine processes data through the channel. The data will be received 3 times in the main goroutine (main).

package main

import (
	"fmt"
	"runtime"
)

func main() {
	runtime.GOMAXPROCS(2)
	var messages = make(chan string)
	var sayHelloTo = func(who string) {
		var data = fmt.Sprintf("hello %s", who)
		messages <- data
	}

	go sayHelloTo("captain america")
	go sayHelloTo("captain marvel")
	go sayHelloTo("black panter")
	
  var message1 = <-messages
	fmt.Println(message1)
	
  var message2 = <-messages
	fmt.Println(message2)
	
  var message3 = <-messages
	fmt.Println(message3)
}

In the above code, the variable messages is declared to be of type channel string. The way to initialize the channel variable is by writing make with the chan keyword followed by the desired channel data type.

var messages = make(chan string)

In this program, we also create a closure sayHelloTo function that returns string data. The data is then sent through the messages channel. The <- sign if written to the left of the variable name then the process is in progress of sending data from the variable on the right through the channel on the left. In this context, the data variable is sent through the messages channel.

var sayHelloTo = func(who string) {
    var data = fmt.Sprintf("hello %s", who)
    messages <- data
}

The sayHelloTo function is executed 3 times as different goroutines. Making these three processes run asynchronously or not waiting for each other.

go sayHelloTo("captain america")
go sayHelloTo("captain marvel")
go sayHelloTo("black panther")

Of the three functions, the earliest goroutine is in charge of sending data and will be received by the variable message1. The <- sign if written to the right of the channel, indicates the process of receiving data from the channel on the right, to be stored in the variable on the left.

var message1 = <-messages
fmt.Println(message1)

Since channel is blocking, it means that the statement var message1 = <-messages until after will not be executed until there is data sent through the channel.

The three goroutines will be received sequentially by message1, message2 and message3 and then displayed.

➜ channel git:(main) ✗ go run main.go 
hello black panter
hello captain marvel
hello captain america
➜ channel git:(main) ✗ go run main.go
hello black panter
hello captain marvel
hello captain america
➜ channel git:(main) ✗ go run main.go
hello black panter
hello captain marvel
hello captain america

It can be seen that the output returned by sayHelloTo is not always sequential, even though the data received is sequential. This is because it sends data from 3 different goroutines, which we don’t know which one was executed first. The earlier the goroutine is executed, the earlier the data will be received.

It has been mentioned above that this channel is blocking so there is no need to use the fmt.Scanln() command to do the blocking.

Channel Data Type Parameter

Channel variables can be passed to other functions as parameters. This is done by adding the chan keyword when declaring it.

Let’s continue with the practice. Set up the printMessage function with a channel parameter, then retrieve the data sent through the channel for display.

func printMessage(what chan string) {
    fmt.Println(<-what)
}

After that change a few lines for implementation in the main function.

func main() {
	runtime.GOMAXPROCS(2)
	var messages = make(chan string)

	for _, each := range []string{"america", "marvel", "panther"} {
		go func(who string) {
			var data = fmt.Sprintf("hello %s", who)
			messages <- data
		}(each)
	}
	
	for i := 0; i < 3; i++ {
		printMessage(messages)
	}
}

The what parameter in the printMessage function is of type channel string. Data passing operations can be performed on that variable, and will also affect the messages variable in the main function.

Passing data of type channel via parameter is implicitly pass by reference, what is passed is the pointer. The output of the above program is the same as the previous program.

 channel-parameter git:(main)  go run main.go 
hello america
hello marvel
hello panther
 channel-parameter git:(main)  go run main.go 
hello panther
hello marvel
hello america

Let’s take a look at how the above program works.

Iteration of Array Data on Initialization

Newly initialized array data can be directly iterated, it’s easy by writing it directly after the range keyword.

for _, each := range []string{"america", "marvel", "panther"} {
  // ...
}

Buffered Channel

Channels are by default un-buffered, not buffered in memory. When a goroutine sends data through a channel, there must be another goroutine in charge of receiving data from the same channel with a blocking handover process. That is, the lines of code in the sending and receiving part of the data will not be processed until the handover process is complete.

The Buffered channel is slightly different. In this type of channel, the number of buffers is determined. This number will determine when we can send data. As long as the amount of data sent does not exceed the buffer amount, the transmission will run asynchronous (no blocking).

When the amount of data sent has exceeded the buffer limit, then sending data can only be done when one of the data has been taken from the channel, so there is an empty channel slot. The reception process itself is blocking.

Here is an example of the application of Buffered Channel by making a program that sending data via buffered channel is asynchronous as long as the amount of data being buffered by the channel does not exceed its buffer capacity.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	runtime.GOMAXPROCS(2)
	messages := make(chan int, 2)
	go func() {
		for {
			i := <-messages
			fmt.Println("receive data", i)
		}
	}()
	for i := 0; i < 5; i++ {
		fmt.Println("send data", i)
		messages <- i
	}
}

In the code above, the second parameter of the make function represents the number of buffers. Now, we need to note that the value of buffered channel starts from 0. So, when the value is 2, it means that the maximum number of buffers is 3, namely 0, 1, 2.

In the example above, there is a goroutine that contains the process of receiving data from the message channel which will then be displayed.

After the data receiving goroutine is executed, the data will be sent through the for loop. In the above program we will send a total of 5 data through the message channel sequentially.

➜ channel-buffered git:(main) ✗ go run main.go
send data 0
send data 1
send data 2
send data 3
receive data 0
receive data 1
receive data 2
receive data 3
send data 4

You can see the result in the output above. The sending of the 4th data is followed by the receiving of data, and both processes run in a blocking manner. Sending data to 0, 1, 2 and 3 will run asynchronously, this is because the channel is determined by the buffer value of 3 (remember, starting from 0). Subsequent transmissions (4th and 5th) will only occur if any of the data from the 4 previously sent data has been received (with blocking data handover). Afterwards, once the channel slot is empty, the handover will return to asynchronous.

As a result of the above output, we can see that the 4th transmission is followed by the reception, and both processes are blocking. Why is that? Because when sending data to 0, 1, 2 and 3 will run asynchronously, this is because the channel we specify has a buffer value of 3 (starting from 0).

The next transmissions (4th and 5th) will only occur if there is one of the 4 data that has previously been sent and has been received (blocking data handover). After the channel slot is empty, the 4th and 5th data will be handed over again with aysnchronous.

comments powered by Disqus