Channels in Go

 This is a study note for YouTube tutorial: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=20036s


Channel basics

You can only create channels using make function.

ch := make(chan int) // chan int means create a integer channel

Below is an example how to use channel to transfer data between goroutines.

var wg = sync.WaitGroup{}

func main() {
ch := make(chan int)
wg.Add(2)
go func() {
i := <-ch
fmt.Println(i)
wg.Done()
}()
go func() {
ch <- 42
wg.Done()
}()
wg.Wait()
}

The result is:
42


Restricting data flow

Be noted that channel operation will block the goroutine when executing. If you have unequal receiver and sender, then the additional goroutines will be blocked and give runtime errors.

func main() {
ch := make(chan int)
wg.Add(3)
go func() {
i := <-ch
fmt.Println(i)
wg.Done()
}()
go func() {
ch <- 42
wg.Done()
}()
go func() {
ch <- 27
wg.Done()
}()
wg.Wait()
}

27 fatal error: all goroutines are asleep - deadlock!

You can use bidirectional communication using channel in goroutines.

func main() {
ch := make(chan int)
wg.Add(2)
go func() {
i := <-ch
fmt.Println(i)
ch <- 27
wg.Done()
}()
go func() {
ch <- 42
fmt.Println(<-ch)
wg.Done()
}()

wg.Wait()
}

42 27

Goroutines can automatically manage the directions of the communication. However in most cases, we would like to make channel in single directions, which means one goroutine is dedicated to be receiver and another be a sender. To do this, you can put channel as function parameter for goroutine to restrict the data flow. Because function parameter can be used to cast a bi channel to a single direction channel.

func main() {
ch := make(chan int)
wg.Add(2)
go func(ch <-chan int) { // Here actually cast bi channel to receiver channel
i := <-ch
fmt.Println(i)
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
wg.Done()
}(ch)

wg.Wait()
}

42

Now the first goroutine is restricted to receive-only, and the second is restricted to send-only.

Buffered channels

You can add a buffer for a channel by simply specifying the second parameter of make function with buffer size.

var wg = sync.WaitGroup{}

func main() {
ch := make(chan int, 50)
wg.Add(2)
go func(ch <-chan int) {
i := <-ch
fmt.Println(i)
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
ch <- 27
wg.Done()
}(ch)

wg.Wait()
}

27 will be buffered instead of throwing a "deadlock" error.



For...range loops with channels

The above code is not a correct way to handle buffered channels. "for...range" loops is preferred in this case.

var wg = sync.WaitGroup{}

func main() {
ch := make(chan int, 50)
wg.Add(2)
go func(ch <-chan int) {
for i := range ch {
fmt.Println(i)
}
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
ch <- 27
wg.Done()
}(ch)

wg.Wait()
}

The result is:

42 27 fatal error: all goroutines are asleep - deadlock!

You can see there is "deadlock" error again, but this time it is not because of the sender goroutine, it is because of the for range loop in the receiver goroutine. Because the receiver goroutine keeps trying to receive data even there is no data sent anymore.

To solve this problem, you can use "close(ch)" function to close channel in the sender goroutine if there is no data to send anymore

var wg = sync.WaitGroup{}

func main() {
ch := make(chan int, 50)
wg.Add(2)
go func(ch <-chan int) {
for i := range ch {
fmt.Println(i)
}
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
ch <- 27
close(ch)
wg.Done()
}(ch)

wg.Wait()
}

The result is:

42 27

In fact, channel has a second return value which you can use for checking if the channel is open or closed.

var wg = sync.WaitGroup{}

func main() {
ch := make(chan int, 50)
wg.Add(2)
go func(ch <-chan int) {
for {
if i, ok := <-ch; ok {
fmt.Println(i)
} else {
break
}
}
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
ch <- 27
close(ch)
wg.Done()
}(ch)

wg.Wait()
}

The result is:

42 27


Select statements

You can use a select statement to manage the workflow in a goroutine. Select statement is similar to switch statement, but select statement is only used for communications. The case statement refers to the communications on different channels.

var logCh = make(chan string)
var doneCh = make(chan struct{})

func main() {
go logger()

logCh <- "test1"
logCh <- "test2"
time.Sleep(100 * time.Millisecond)
doneCh <- struct{}{} // empty struct used as a signal here
}

func logger() {
for {
select {
case log := <-logCh:
fmt.Println(log)
case <-doneCh:
break
}
}
}


References: https://www.youtube.com/watch?v=YS4e4q9oBaU&t=20036s

Comments

Popular posts from this blog

Basic understanding of TLS-PSK protocol

Differences between ASIC, ASSP and ASIP

Orthogonal instruction set