[Golang] Channel

2021-12-13 hit count image

Let's see what the Channel is and how to use the Channel to send and receive messages between Goroutines in Golang.

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!

Posts