Contents
Outline
In this blog post, I will introduce the Channel
and how to use it to send and receive messages between Goroutines in Golang. You can see the full source code of this blog post on the link below.
If you want to know details about Goroutine, please read the blog post below.
Channel
The channel is a message queue(Threa-safe queue) to send messages between Goroutines.
In Golang, you can use the make()
function to create the instance of the Channel
like the below.
// var VARIABLE_NAME chan MESSAGE_TYPE = make(chan MESSAGE_TYPE)
var messages chan string = make(chan string)
After creating the channel, you can use the <-
operator to send the data via the channel.
// CHANNEL <- DATA
messages <- "This is a message"
Also, you can use the <-
operator to receive the data via the channel.
// var VARIABLE <- CHANNEL
var msg string = <- messages
To check this, create the main.go
file and modify it like the below.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
go square(&wg, ch)
time.Sleep(time.Second)
ch <- 3
wg.Wait()
}
func square(wg *sync.WaitGroup, ch chan int) {
fmt.Println("Square start")
n := <-ch
fmt.Println("Square:", n*n)
wg.Done()
fmt.Println("Square end")
}
When you run the above code, you will see the following output.
# go run main.go
Square start
Square: 9
Square end
Let’s see the details of the code. First, we used the WaitGroup
to make the main Goroutine wait for a sub Goroutine to finish.
func main() {
var wg sync.WaitGroup
...
wg.Add(1)
...
wg.Wait()
}
And then, we used the make
function to create the channel, and used the go
keyword to run the square
function in a sub Goroutine. At this time, we passed the WaitGroup
and the channel.
func main() {
...
ch := make(chan int)
...
go square(&wg, ch)
...
}
The square
function reads the data from the channel and calculate it and print it. After then, it used the Done
function of the WaitGroup
to notify the main Goroutine that the sub Goroutine has finished.
func square(wg *sync.WaitGroup, ch chan int) {
...
n := <-ch
...
wg.Done()
...
}
When the n := <-ch
code is executed, the code try to get the data from the channel. However, there is no data yet in the channel, so it will wait until other Goroutines send the data to the channel.
At this moment, the main Goroutine sends the data by using ch <- 3
, and the sub Goroutine receives it and processes it and calls wg.Done()
. So, the main Goroutine won’t wait for the sub Goroutine anymore and exits.
Channel size
The default channel size is 0
. So, if you write the code like the below, the Goroutine will wait until other Goroutines receive the data from the channel.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go square()
ch <- 9
fmt.Println("Never Print")
}
func square() {
for {
time.Sleep(2 * time.Second)
fmt.Println("sleep")
}
}
Because the channel size is 0, Goroutine can’t add the data to the channel. If there is a receiver code, the Goroutine can send the data through the channel directly, so it doesn’t matter. However, if there is no receiver code, the Goroutine will wait until other Goroutines send the data to the channel forever.
Next, let’s make the channel size to be 1 like the below.
package main
import (
"fmt"
"time"
)
func main() {
// ch := make(chan int)
ch := make(chan int, 1)
go square()
ch <- 9
fmt.Println("Never Print")
}
func square() {
for {
time.Sleep(2 * time.Second)
fmt.Println("sleep")
}
}
If you make the channel with the size, the channel can store the size of data that you defined. So, if there is no receiver code in Goroutine, the channel can store the data, so the data is stored on the channel, and the Goroutine will be able to proceed.
Wait for the data from Channel
If you use the channel like the below, you can make the Goroutine wait for the data to be added.
for n := range ch {
...
}
To check this, modify the main.go
file like the below.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
go square(&wg, ch)
for i := 0; i < 10; i++ {
ch <- i * 2
}
wg.Wait()
}
func square(wg *sync.WaitGroup, ch chan int) {
for n := range ch {
fmt.Println("Square:", n*n)
time.Sleep(time.Second)
}
fmt.Println("Done")
wg.Done()
}
When the code is executed, you will see the following result.
# go run main.go
Square: 100
Square: 144
Square: 196
Square: 256
Square: 324
fatal error: all goroutines are asleep - deadlock!
The data is added to the channel in the main function. However, there is no size of the channel, so the Goroutine will wait until other Goroutines receive the data from the channel. At this time, the square
function on the sub Goroutine reads the data if there is a data.
So, the main function wants to add the data 10 times continuously, but the channel size is 0, so the Goroutine waits. At this moment, the square
function reads data if there is the data, so the main function sends the data and the square
function receives it and prints it. And then, the main function sends the data again and the square
function reads the data and prints it again.
After 10 times of this, the main function doesn’t add the data to the channel anymore, but the square
function waits for the data to be added forever, so the sub Goroutine won’t exit and there is no action, so you can see the deadlock
occurs.
Close Channel
When you use the code that waits for the data to be added like above, Goroutine will wait forever, and it can be Zombie Goroutine or Goroutine leak occurs.
To prevent this, you must close the channel by the close
function after you finish using the channel.
To check this, modify the main.go
file like the below.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
go square(&wg, ch)
for i := 0; i < 10; i++ {
ch <- i * 2
}
close(ch)
wg.Wait()
}
func square(wg *sync.WaitGroup, ch chan int) {
for n := range ch {
fmt.Println("Square:", n*n)
time.Sleep(time.Second)
}
fmt.Println("Done")
wg.Done()
}
When the code is executed, you will see the following result.
# go run main.go
Square: 100
Square: 144
Square: 196
Square: 256
Square: 324
Done
Unlike the previous example, you can see the deadlock doesn’t occur. we close the channel by close(ch)
after adding all messages to the channel in the main function. Therefore, the square
function doesn’t need to wait for the data to be added, so the sub Goroutine exits. So, there is no deadlock like before.
select statement
You can use the select
statement to wait for the data from multiple channels like the below.
select {
case n := <- ch1:
...
case n := <- ch2:
...
case ...
}
To check this, modify the main.go
file like the below.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
go square(&wg, ch)
for i := 0; i < 10; i++ {
ch <- i * 2
}
close(ch)
wg.Wait()
}
func square(wg *sync.WaitGroup, ch chan int) {
tick := time.Tick(time.Second)
terminate := time.After(10 * time.Second)
for {
select {
case <-tick:
fmt.Println("Tick")
case <-terminate:
fmt.Println("Terminate")
wg.Done()
return
case n := <-ch:
fmt.Println("Square:", n*n)
time.Sleep(time.Second)
}
}
}
When you run the code, you will see the following result.
# go run main.go
Square: 0
Square: 4
Square: 16
Tick
Square: 36
Square: 64
Tick
Square: 100
Tick
Square: 144
Tick
Square: 196
Tick
Square: 256
Square: 324
Square: 0
Terminate
The Tick()
function in the time
package returns a channel that signals at regular intervals. The After()
returns a channel that signals only once after watiting for the specified duration. Here, we implement to wait for the data from the multiple channels by using them and the select
statement.
Unidirectional Channel
In Golang, you can make unidirectional channel like the below.
// The channel only can add the data
chan <- string
// The channel only can receive the data
<- chan string
To check this, modify the main.go
file like the below.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
go square(&wg, ch)
for i := 0; i < 10; i++ {
ch <- i * 2
}
close(ch)
wg.Wait()
}
func square(wg *sync.WaitGroup, ch <-chan int) {
for n := range ch {
fmt.Println("Square:", n*n)
time.Sleep(time.Second)
}
fmt.Println("Done")
wg.Done()
}
When you run the code, you will see the following result.
# go run main.go
Square: 0
Square: 4
Square: 16
Square: 36
Square: 64
Square: 100
Square: 144
Square: 196
Square: 256
Square: 324
Done
The unidirectional channel is used for making sure the role of the channel. In the example, the square
function uses the channel only to read the data. At this time, we can use the unidirectional channel to specify the role. After specifying the role, if you try to add the data to the channel accidentally, the compile error occurs, so you can use the channel more safely.
Completed
Done! we’ve seen what the channel is and how to use it to send and receive data between Goroutines in Golang. Also, we’ve seen how to manage the channel size and how to make Goroutine wait for the data to be added.
Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!
App promotion
Deku
.Deku
created the applications with Flutter.If you have interested, please try to download them for free.